From aadc3153811c3920bbb66d69c2bf1a753a0e5b18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Falc=C3=A3o?= Date: Mon, 1 Jan 2024 04:02:52 +0000 Subject: [PATCH] more improvements --- docs/source/api-reference.rst | 5 ++-- sure/__init__.py | 10 ++++---- sure/cli.py | 2 ++ sure/core.py | 29 ++++------------------- sure/loader.py | 44 ++++++++++++++++++++++++++++++++++- sure/original.py | 4 ++-- sure/runtime.py | 13 +++++------ tests/test_original_api.py | 12 +++++----- 8 files changed, 71 insertions(+), 48 deletions(-) diff --git a/docs/source/api-reference.rst b/docs/source/api-reference.rst index e709867..043348e 100644 --- a/docs/source/api-reference.rst +++ b/docs/source/api-reference.rst @@ -30,8 +30,6 @@ API Reference .. autoclass:: sure.core.DeepExplanation .. _deep comparison: .. autoclass:: sure.core.DeepComparison -.. autofunction:: sure.core._get_file_name -.. autofunction:: sure.core._get_line_number .. autofunction:: sure.core.itemize_length @@ -66,6 +64,7 @@ API Reference .. py:module:: sure.reporters .. autoclass:: sure.reporters.feature.FeatureReporter + ``sure.original`` ----------------- @@ -75,7 +74,6 @@ API Reference .. autofunction:: sure.original.all_integers .. autofunction:: sure.original.explanation - ``sure.doubles`` ---------------- @@ -84,6 +82,7 @@ API Reference .. autoclass:: sure.doubles.FakeOrderedDict .. autoattribute:: sure.doubles.anything + ``sure.doubles.dummies`` ------------------------ diff --git a/sure/__init__.py b/sure/__init__.py index ba44fbf..e719b0e 100644 --- a/sure/__init__.py +++ b/sure/__init__.py @@ -35,11 +35,11 @@ from sure import runtime from sure.core import DeepComparison from sure.core import DeepExplanation -from sure.core import _get_file_name -from sure.core import _get_line_number from sure.errors import SpecialSyntaxDisabledError from sure.errors import InternalRuntimeError from sure.doubles.dummies import anything +from sure.loader import get_file_name +from sure.loader import get_line_number from sure.version import version from sure.special import is_cpython, patchable_builtin from sure.registry import context as _registry @@ -339,8 +339,8 @@ def ensure_providers(func, attr, args, kwargs): def check_dependencies(func): action = func.__name__ - filename = _get_file_name(func) - lineno = _get_line_number(func) + filename = get_file_name(func) + lineno = get_line_number(func) for dependency in depends_on: if dependency in context.__sure_providers_of__: @@ -354,7 +354,7 @@ def check_dependencies(func): err += "\n".join( [ " -> %s at %s:%d" - % (p.__name__, _get_file_name(p), _get_line_number(p)) + % (p.__name__, get_file_name(p), get_line_number(p)) for p in providers ] ) diff --git a/sure/cli.py b/sure/cli.py index 5a0ff39..545d557 100644 --- a/sure/cli.py +++ b/sure/cli.py @@ -83,6 +83,8 @@ def entrypoint(paths, reporter, immediate, log_level, log_file, special_syntax, raise ExitError(runner.context, result) elif cov: + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ cov.stop() cov.save() cov.report() diff --git a/sure/core.py b/sure/core.py index 367a3b9..5126e29 100644 --- a/sure/core.py +++ b/sure/core.py @@ -14,10 +14,8 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os -import inspect - from collections import OrderedDict +from functools import cache from six import ( text_type, integer_types, string_types, binary_type, get_function_code @@ -50,6 +48,8 @@ def __init__(self, X, Y, epsilon=None, parent=None): float: self.compare_floats, dict: self.compare_ordered_dicts, list: self.compare_iterables, + set: self.compare_iterables, + frozenset: self.compare_iterables, tuple: self.compare_iterables, OrderedDict: self.compare_ordered_dicts } @@ -144,10 +144,8 @@ def compare_ordered_dicts(self, X, Y): return DeepExplanation(msg) return True + @cache def get_context(self): - if self._context: - return self._context - X_keys = [] Y_keys = [] @@ -168,8 +166,7 @@ class ComparisonContext: current_Y_keys = get_keys(Y_keys) parent = comp - self._context = ComparisonContext() - return self._context + return ComparisonContext() def compare_iterables(self, X, Y): len_X, len_Y = map(len, (X, Y)) @@ -242,22 +239,6 @@ def explanation(self): return self._explanation -def _get_file_name(func): - try: - name = inspect.getfile(func) - except AttributeError: - name = get_function_code(func).co_filename - - return os.path.abspath(name) - - -def _get_line_number(func): - try: - return inspect.getlineno(func) - except AttributeError: - return get_function_code(func).co_firstlineno - - def itemize_length(items): length = len(items) return '{0} item{1}'.format(length, length > 1 and "s" or "") diff --git a/sure/loader.py b/sure/loader.py index 93274e9..04ac427 100644 --- a/sure/loader.py +++ b/sure/loader.py @@ -1,9 +1,11 @@ import os import sys import ast +import types import importlib import importlib.util -from typing import Dict, List, Union, Tuple + +from typing import Dict, List, Optional, Tuple, Union from importlib.machinery import PathFinder from pathlib import Path from sure.errors import InternalRuntimeError @@ -13,6 +15,42 @@ __TEST_CLASSES__ = {} +def get_file_name(func) -> str: + """returns the file name of a given function or method""" + return FunMeta.from_function_or_method(func).filename + + +def get_line_number(func) -> str: + """returns the first line number of a given function or method""" + return FunMeta.from_function_or_method(func).line_number + + +class FunMeta(object): + """container for metadata specific to Python functions or methods""" + filename: str + line_number: int + name: str + + def __init__(self, filename: str, line_number: int, name: str): + self.filename = collapse_path(filename) + self.line_number = line_number + self.name = name + + def __repr__(self): + return f'' + + @classmethod + def from_function_or_method(cls, func): + if not isinstance(func, (types.FunctionType, types.MethodType)): + raise TypeError(f'get_function_or_method_metadata received an unexpected object: {func}') + + return cls( + filename=func.__code__.co_filename, + line_number=func.__code__.co_firstlineno, + name=func.__name__, + ) + + def name_appears_to_indicate_test(name: str) -> bool: return name.startswith('Test') or name.endswith('Test') @@ -93,6 +131,10 @@ def resolve_path(path, relative_to="~") -> Path: return Path(path).absolute().relative_to(Path(relative_to).expanduser()) +def collapse_path(e: Union[str, Path]) -> str: + return str(e).replace(os.getenv("HOME"), "~") + + def get_package(path) -> Path: if not isinstance(path, Path): path = Path(path) diff --git a/sure/original.py b/sure/original.py index 96ef93f..df8a5b1 100644 --- a/sure/original.py +++ b/sure/original.py @@ -35,9 +35,9 @@ from six import string_types, text_type from sure.core import DeepComparison -from sure.core import _get_file_name -from sure.core import _get_line_number from sure.core import itemize_length +from sure.loader import get_file_name +from sure.loader import get_line_number def identify_callable_location(callable_object): diff --git a/sure/runtime.py b/sure/runtime.py index 6aa91e1..2f88f82 100644 --- a/sure/runtime.py +++ b/sure/runtime.py @@ -38,6 +38,7 @@ ) from sure.loader import ( loader, + collapse_path, get_type_definition_filename_and_firstlineno, ) from sure.reporter import Reporter @@ -117,10 +118,12 @@ def __init__(self, test, module_or_instance=None): self.name = test.__class__.__name__ self.filename, self.line = get_type_definition_filename_and_firstlineno(test.__class__) self.kind = test.__class__ + elif isinstance(test, type): self.name = test.__name__ self.filename, self.line = get_type_definition_filename_and_firstlineno(test) self.kind = test + else: raise NotImplementedError(f"{test} of type {type(test)} is not yet supported by {TestLocation}") @@ -765,7 +768,7 @@ def __getattr__(self, attr): try: return self.__getattribute__(attr) except AttributeError: - return getattr(self.scenario_results[-1], attr, fallback) + return getattr(self.scenario_results[-1], attr) @property def is_failure(self): @@ -843,7 +846,7 @@ def __getattr__(self, attr): try: return self.__getattribute__(attr) except AttributeError: - return getattr(self.scenario_results[-1], attr, fallback) + return getattr(self.scenario_results[-1], attr) @property def is_failure(self): @@ -920,7 +923,7 @@ def __getattr__(self, attr): try: return self.__getattribute__(attr) except AttributeError: - return getattr(self.feature_results[-1], attr, fallback) + return getattr(self.feature_results[-1], attr) @property def is_failure(self): @@ -957,7 +960,3 @@ def first_nonsuccessful_result(self) -> Optional[FeatureResult]: def stripped(string): return collapse_path("\n".join(filter(bool, [s.strip() for s in string.splitlines()]))) - - -def collapse_path(e: str): - return str(e).replace(os.getenv("HOME"), "~") diff --git a/tests/test_original_api.py b/tests/test_original_api.py index df9a5f3..c466178 100644 --- a/tests/test_original_api.py +++ b/tests/test_original_api.py @@ -15,11 +15,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os import sure from sure import that from sure import expect from sure import VariablesBag from sure.special import is_cpython +from sure.loader import collapse_path def test_setup_with_context(): @@ -52,7 +54,7 @@ def it_crashes(): assert that(it_crashes).raises( TypeError, ( - "the function it_crashes defined at test_original_api.py line 49, is being " + "the function it_crashes defined at test_original_api.py line 51, is being " "decorated by either @that_with_context or @scenario, so it should " "take at least 1 parameter, which is the test context" ), @@ -925,12 +927,11 @@ def the_providers_are_working(the): def test_depends_on_failing_due_to_lack_of_attribute_in_context(): "it fails when an action depends on some attribute that is not " "provided by any other previous action" - import os from sure import action_for, scenario - fullpath = os.path.abspath(__file__) + fullpath = collapse_path(collapse_path(os.path.abspath(__file__))) error = ( - 'the action "variant_action" defined at %s:940 ' + 'the action "variant_action" defined at %s:941 ' 'depends on the attribute "data_structure" to be available in the' " context. It turns out that there are no actions providing " "that. Please double-check the implementation" % fullpath @@ -952,10 +953,9 @@ def depends_on_fails(the): def test_depends_on_failing_due_not_calling_a_previous_action(): "it fails when an action depends on some attribute that is being " "provided by other actions" - import os from sure import action_for, scenario - fullpath = os.path.abspath(__file__) + fullpath = collapse_path(os.path.abspath(__file__)) error = ( 'the action "my_action" defined at {0}:971 ' 'depends on the attribute "some_attr" to be available in the context.'