From 2f8a5c7cb111b861dbfe6a5d1774d3f579376583 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Sat, 24 Feb 2024 21:27:34 +0300 Subject: [PATCH 01/52] mutation testing mmachinery --- .gitignore | 2 ++ pyproject.toml | 5 +++++ requirements_dev.txt | 1 + 3 files changed, 8 insertions(+) diff --git a/.gitignore b/.gitignore index 31d0432..210879c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ build .mypy_cache .ruff_cache test.py +.mutmut-cache +html diff --git a/pyproject.toml b/pyproject.toml index 8268f4e..ace46ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,11 @@ classifiers = [ [tool.setuptools.package-data] "fazy" = ["py.typed"] + +[tool.mutmut] +paths_to_mutate="f" +runner="pytest" + [project.urls] 'Source' = 'https://github.com/pomponchik/fazy' 'Tracker' = 'https://github.com/pomponchik/fazy/issues' diff --git a/requirements_dev.txt b/requirements_dev.txt index abe3089..03248ae 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -5,3 +5,4 @@ twine==4.0.2 build==0.9.0 ruff==0.0.290 mypy==1.4.1 +mutmut==2.4.4 From 61e0d8b426b42e80c6bedca360955158ada38135 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Mon, 17 Jun 2024 13:33:11 +0300 Subject: [PATCH 02/52] some dunders --- f/proxy_module.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/f/proxy_module.py b/f/proxy_module.py index 4830cbc..d74dab0 100644 --- a/f/proxy_module.py +++ b/f/proxy_module.py @@ -4,7 +4,7 @@ import inspect from string import Formatter from types import CodeType, FrameType -from typing import Iterable, Optional, Union, Sized, Dict, Callable, Type, Any +from typing import Set, Iterable, Optional, Union, Sized, Dict, Callable, Type, Any try: from typing import Protocol except ImportError: @@ -43,6 +43,12 @@ def __call__(self, string: Union[LazyString, str], lazy: bool = True, safe: bool return result return result.data + def __str__(self) -> str: + return 'f' + + def __repr__(self) -> str: + return 'f' + def sum_of_nonlocals(self, first_frame: Optional[FrameType], base_qualname: Optional[str], closures: bool, safe: bool) -> Dict[str, Any]: if not closures or first_frame is None or base_qualname is None: return {} @@ -152,8 +158,13 @@ def startswith(iterable: SizedAndIterable, second_iterable: SizedAndIterable) -> return True - def __str__(self) -> str: - return 'f' + def get_all_mentioned_names(self, string: str) -> Set[str]: + result = set() - def __repr__(self) -> str: - return 'f' + class Visiter(ast.NodeVisitor): + def visit_Name(_self, node: ast.AST) -> Optional[ast.AST]: + result.add(node.id) + + Visiter().visit(ast.parse(string)) + + return result From 603e8c02877ff6e29c819280330238dcd09b5446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 17:13:17 +0300 Subject: [PATCH 03/52] issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 32 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/documentation.md | 26 ++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 17 ++++++++++++ .github/ISSUE_TEMPLATE/question.md | 12 +++++++++ 4 files changed, 87 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/documentation.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/question.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..fe1edf2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: pomponchik + +--- + +## Short description + +Replace this text with a short description of the error and the behavior that you expected to see instead. + + +## Describe the bug in detail + +Please add this test in such a way that it reproduces the bug you found and does not pass: + +```python +def test_your_bug(): + ... +``` + +Writing the test, please keep compatibility with the [`pytest`](https://docs.pytest.org/) framework. + +If for some reason you cannot describe the error in the test format, describe here the steps to reproduce it. + + +## Environment + - OS: ... + - Python version (the output of the `python --version` command): ... + - Version of this package: ... diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 0000000..20f4742 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,26 @@ +--- +name: Documentation fix +about: Add something to the documentation, delete it, or change it +title: '' +labels: documentation +assignees: pomponchik +--- + +## It's cool that you're here! + +Documentation is an important part of the project, we strive to make it high-quality and keep it up to date. Please adjust this template by outlining your proposal. + + +## Type of action + +What do you want to do: remove something, add it, or change it? + + +## Where? + +Specify which part of the documentation you want to make a change to? For example, the name of an existing documentation section or the line number in a file `README.md`. + + +## The essence + +Please describe the essence of the proposed change diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..3d12c06 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: pomponchik + +--- + +## Short description + +What do you propose and why do you consider it important? + + +## Some details + +If you can, provide code examples that will show how your proposal will work. Also, if you can, indicate which alternatives to this behavior you have considered. And finally, how do you propose to test the correctness of the implementation of your idea, if at all possible? diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..1a8a31c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,12 @@ +--- +name: Question or consultation +about: Ask anything about this project +title: '' +labels: guestion +assignees: pomponchik + +--- + +## Your question + +Here you can freely describe your question about the project. Please, before doing this, read the documentation provided, and ask the question only if the necessary answer is not there. In addition, please keep in mind that this is a free non-commercial project and user support is optional for its author. The response time is not guaranteed in any way. From a203b046fce0d0fc0a0a21d1bfc571a59acc8b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 17:16:39 +0300 Subject: [PATCH 04/52] added python 3.13 and deleted python 3.7 support in the CI --- .github/workflows/lint.yml | 14 +++++++++++--- .github/workflows/tests_and_coverage.yml | 7 +++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 85c7a79..de91ff7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,13 +9,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} @@ -25,8 +25,16 @@ jobs: - name: Run ruff shell: bash - run: ruff f + run: ruff check f - name: Run mypy shell: bash run: mypy f --strict + + - name: Run mypy for tests + shell: bash + run: mypy tests + + - name: Run ruff for tests + shell: bash + run: ruff check tests diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml index c1c1bb4..8f4685d 100644 --- a/.github/workflows/tests_and_coverage.yml +++ b/.github/workflows/tests_and_coverage.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v2 @@ -28,7 +28,7 @@ jobs: run: pip install -r requirements_dev.txt - name: Run tests and show coverage on the command line - run: coverage run --source=f --omit="*tests*" -m pytest --cache-clear && coverage report -m + run: coverage run --source=f --omit="*tests*" -m pytest --cache-clear && coverage report -m --fail-under=100 - name: Upload reports to codecov env: @@ -39,3 +39,6 @@ jobs: find . -iregex "codecov.*" chmod +x codecov ./codecov -t ${CODECOV_TOKEN} + + - name: Run tests and show the branch coverage on the command line + run: coverage run --branch --source=f --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m --fail-under=100 From 2c52515c008cfb3193c08a8b78d7418bef39c7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 17:17:03 +0300 Subject: [PATCH 05/52] new versions of dev deps --- requirements_dev.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 03248ae..7d8c6a1 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,8 +1,8 @@ -pytest==7.2.2 -coverage==7.2.2 +pytest==7.4.3 +coverage==7.6.1 wheel==0.40.0 -twine==4.0.2 -build==0.9.0 -ruff==0.0.290 -mypy==1.4.1 -mutmut==2.4.4 +twine==6.1.0 +build==1.2.2.post1 +ruff==0.9.9 +mypy==1.14.1 +mutmut==3.2.3 From b26c02e0d95446c45f39fe2321a03f1d04fd15dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 17:17:44 +0300 Subject: [PATCH 06/52] classifiers --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ace46ea..79cfa7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,19 +10,19 @@ authors = [ ] description = 'Lazy f-strings for everyone' readme = 'README.md' -requires-python = '>=3.7' +requires-python = '>=3.8' classifiers = [ 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', From 5511ee4dab3b895956524c44822158ac3342ae54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 19:15:11 +0300 Subject: [PATCH 07/52] deleted all AST-related things from the code --- f/proxy_module.py | 80 +++------------------------------------------- tests/test_init.py | 40 ----------------------- 2 files changed, 5 insertions(+), 115 deletions(-) diff --git a/f/proxy_module.py b/f/proxy_module.py index d74dab0..eff0980 100644 --- a/f/proxy_module.py +++ b/f/proxy_module.py @@ -1,6 +1,5 @@ import gc import sys -import ast import inspect from string import Formatter from types import CodeType, FrameType @@ -20,7 +19,7 @@ class SizedAndIterable(Sized, Iterable[Any], Protocol): class ProxyModule(sys.modules[__name__].__class__): # type: ignore[misc] old_str = str - def __call__(self, string: Union[LazyString, str], lazy: bool = True, safe: bool = True, closures: bool = True) -> Union[LazyString, str]: + def __call__(self, string: Union[LazyString, str], lazy: bool = True, closures: bool = True) -> Union[LazyString, str]: if isinstance(string, LazyString): return string @@ -32,9 +31,8 @@ def __call__(self, string: Union[LazyString, str], lazy: bool = True, safe: bool {**base_frame.f_globals}, self.sum_of_nonlocals( base_frame.f_back, - self.get_qualname(base_frame.f_code, raise_if_not_literal=safe, code_line=base_frame.f_lineno), + self.get_qualname(base_frame.f_code, code_line=base_frame.f_lineno), closures, - safe, ), lazy, ) @@ -49,7 +47,7 @@ def __str__(self) -> str: def __repr__(self) -> str: return 'f' - def sum_of_nonlocals(self, first_frame: Optional[FrameType], base_qualname: Optional[str], closures: bool, safe: bool) -> Dict[str, Any]: + def sum_of_nonlocals(self, first_frame: Optional[FrameType], base_qualname: Optional[str], closures: bool) -> Dict[str, Any]: if not closures or first_frame is None or base_qualname is None: return {} @@ -57,7 +55,7 @@ def sum_of_nonlocals(self, first_frame: Optional[FrameType], base_qualname: Opti while first_frame is not None: code = first_frame.f_code - qualname = self.get_qualname(code, raise_if_not_literal=False, code_line=0) + qualname = self.get_qualname(code, code_line=0) if qualname is not None: if self.startswith(base_qualname.split('.'), qualname.split('.')): all_locals.append(first_frame.f_locals) @@ -74,7 +72,7 @@ def sum_of_nonlocals(self, first_frame: Optional[FrameType], base_qualname: Opti return result @classmethod - def get_qualname(cls: Type['ProxyModule'], code: CodeType, raise_if_not_literal: bool, code_line: int) -> Optional[str]: + def get_qualname(cls: Type['ProxyModule'], code: CodeType, code_line: int) -> Optional[str]: functions = [] for function in gc.get_referrers(code): @@ -88,65 +86,8 @@ def get_qualname(cls: Type['ProxyModule'], code: CodeType, raise_if_not_literal: if functions: function = functions[0] - if raise_if_not_literal: - cls.check_code(function, code, code_line) return function.__qualname__ # type: ignore[no-any-return] - @staticmethod - def check_code(function: Callable[..., Any], code: CodeType, code_line: int) -> None: - try: - if inspect.isgenerator(function): - code_strings, begin_code_line_number = inspect.getsourcelines(code) - else: - code_strings, begin_code_line_number = inspect.getsourcelines(function) - except Exception: - return - - spaces_count = 0 - for letter in code_strings[0]: - if not letter.isspace(): - break - spaces_count += 1 - - code_strings = [x[spaces_count:] for x in code_strings] - - full_code = ''.join(code_strings) - ast_of_code = ast.parse(full_code) - - flag: Union[int, bool] = True - class ConstantVisitor(ast.NodeVisitor): - def visit_Call(self, node: ast.Call) -> None: - nonlocal flag - if node.lineno + begin_code_line_number - 1 == code_line: - if hasattr(node.func, 'id') and node.func.id == 'f': - if len(node.args) == 1 and isinstance(node.args[0], ast.Constant): - flag *= True - elif len(node.args) == 1 and isinstance(node.args[0], ast.Call) and hasattr(node.args[0].func, 'id') and node.args[0].func.id == 'f': - flag *= True - for arg in node.args: - ConstantVisitor().visit(arg) - for keyword in node.keywords: - ConstantVisitor().visit(keyword.value) - else: - flag *= False - elif isinstance(node.func, ast.Attribute): - ConstantVisitor().visit(node.func.value) - for arg in node.args: - ConstantVisitor().visit(arg) - for keyword in node.keywords: - ConstantVisitor().visit(keyword.value) - else: - for arg in node.args: - ConstantVisitor().visit(arg) - for keyword in node.keywords: - ConstantVisitor().visit(keyword.value) - - ConstantVisitor().visit(ast_of_code) - - if not flag and not (sys.version_info < (3, 8)): - raise SyntaxError('Unsafe use of a variable as a template.') - - @staticmethod def startswith(iterable: SizedAndIterable, second_iterable: SizedAndIterable) -> bool: if len(iterable) < len(second_iterable): @@ -157,14 +98,3 @@ def startswith(iterable: SizedAndIterable, second_iterable: SizedAndIterable) -> return False return True - - def get_all_mentioned_names(self, string: str) -> Set[str]: - result = set() - - class Visiter(ast.NodeVisitor): - def visit_Name(_self, node: ast.AST) -> Optional[ast.AST]: - result.add(node.id) - - Visiter().visit(ast.parse(string)) - - return result diff --git a/tests/test_init.py b/tests/test_init.py index e0bc8ae..037238f 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -298,43 +298,3 @@ def wrapped(): with pytest.raises(NameError): assert wrapper()() - - -@pytest.mark.skipif(sys.version_info < (3, 8), reason='Problems with Python 3.7') -def test_string_as_variable_when_safe_mode(): - # default mode is True - with pytest.raises(SyntaxError): - string = 'kek' - f(string) - - with pytest.raises(SyntaxError): - string = 'kek' - f(string, safe=True) - - -@pytest.mark.skipif(sys.version_info < (3, 8), reason='Problems with Python 3.7') -def test_string_as_variable_when_safe_mode_into_generator_function(): - def generator(): - string = 'kek' - yield f(string) - - with pytest.raises(SyntaxError): - for _ in generator(): - pass - - -@pytest.mark.skipif(sys.version_info < (3, 8), reason='Problems with Python 3.7') -def test_string_as_variable_when_safe_mode_into_generator_expression(): - with pytest.raises(SyntaxError): - list(f(string) for string in ['lol', 'kek']) - - -@pytest.mark.skipif(sys.version_info < (3, 8), reason='Problems with Python 3.7') -def test_string_as_variable_when_safe_mode_into_double_strings_generator_expression(): - with pytest.raises(SyntaxError): - list((f(string), f('kek')) for string in ['lol', 'kek']) - - -def test_string_as_variable_when_not_safe_mode(): - string = 'kek' - assert f(string, safe=False) == string From f71c0ee65cba3e9e6d71cc6c49177553ac6847c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 19:21:09 +0300 Subject: [PATCH 08/52] no extra imports --- f/proxy_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/f/proxy_module.py b/f/proxy_module.py index eff0980..a167895 100644 --- a/f/proxy_module.py +++ b/f/proxy_module.py @@ -3,7 +3,7 @@ import inspect from string import Formatter from types import CodeType, FrameType -from typing import Set, Iterable, Optional, Union, Sized, Dict, Callable, Type, Any +from typing import Iterable, Optional, Union, Sized, Dict, Type, Any try: from typing import Protocol except ImportError: From e565a9bcd6c1f2c520ec9bca8d610e70d52f7a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 19:24:38 +0300 Subject: [PATCH 09/52] import Protocol only from the standard library --- f/proxy_module.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/f/proxy_module.py b/f/proxy_module.py index a167895..2bd355a 100644 --- a/f/proxy_module.py +++ b/f/proxy_module.py @@ -3,11 +3,7 @@ import inspect from string import Formatter from types import CodeType, FrameType -from typing import Iterable, Optional, Union, Sized, Dict, Type, Any -try: - from typing import Protocol -except ImportError: - from typing_extensions import Protocol # type: ignore[assignment] +from typing import Protocol, Iterable, Optional, Union, Sized, Dict, Type, Any from f.chain_unit import ChainUnit from f.lazy_string import LazyString From 1b0fe864131e8270295c717c0e7e9371ee6a7821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 19:32:15 +0300 Subject: [PATCH 10/52] fix lint issues --- tests/test_dunders.py | 2 +- tests/test_init.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_dunders.py b/tests/test_dunders.py index a6e0f47..55c733d 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -67,7 +67,7 @@ def test_dunder_contains(): def test_dunder_len(): - kek = '1234567890' + kek = '1234567890' # noqa: F841 assert len(f('lol')) == 3 assert len(f('{kek}')) == 10 diff --git a/tests/test_init.py b/tests/test_init.py index 037238f..04209e8 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,5 +1,4 @@ import os -import sys import logging from io import StringIO from contextlib import redirect_stdout @@ -39,7 +38,7 @@ def test_globals_and_locals_intersection(): def test_complex_string(): - kek = 'kek?' + kek = 'kek?' # noqa: F841 assert f('lol {kek} {"cheburek"} {GLOBAL_VARIABLE} {2} {False}') == 'lol kek? cheburek kek 2 False' From 6d3a776fcb801ee4f0eb61889b787f26d6591211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 19:35:19 +0300 Subject: [PATCH 11/52] fix lint issues --- tests/test_init.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index 04209e8..2aae564 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -166,13 +166,13 @@ def function(): def test_read_nonlocal_variable_nested(): - kek = 5 + kek = 5 # noqa: F841 def function_2(): return f('{kek}') def function(): - kek = 3 + kek = 3 # noqa: F841 return function_2() assert function() == '{0}'.format(5) @@ -182,7 +182,7 @@ def function_2(): return '{0}'.format(kek) def function(): - kek = 3 + kek = 3 # noqa: F841 return function_2() assert function() == '{0}'.format(5) @@ -279,7 +279,7 @@ def test_not_lazy_mode(): def test_no_closures_mode_base_working(): - number = 5 + number = 5 # noqa: F841 assert f('kek', closures=False) == 'kek' assert f('kek {number}', closures=False) == 'kek 5' From cbb6409918921bc59f4d73ed29aac2142f4fa820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 19:39:41 +0300 Subject: [PATCH 12/52] fix lint issues --- tests/test_init.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index 2aae564..17c8bcf 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -287,10 +287,10 @@ def test_no_closures_mode_base_working(): def test_raise_if_closures_when_no_closures_mode(): - number_1 = 5 + number_1 = 5 # noqa: F841 def wrapper(): - number_2 = 10 + number_2 = 10 # noqa: F841 def wrapped(): return f('kek {number_1} {number_2}', closures=False) return wrapped From 5181610707e9d65631fdd9443f2330c95d1c48b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 19:45:43 +0300 Subject: [PATCH 13/52] fix lint issues --- tests/test_skipped.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_skipped.py b/tests/test_skipped.py index 8e99af1..722a8c6 100644 --- a/tests/test_skipped.py +++ b/tests/test_skipped.py @@ -1,3 +1,4 @@ +import os import sys import pickle from tempfile import TemporaryDirectory From 1750e81c178214b395eb18b70b2b1665ca493a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 19:48:07 +0300 Subject: [PATCH 14/52] fix lint issues --- tests/test_init.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index 17c8bcf..ee8273c 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -79,8 +79,6 @@ def __str__(self): accumulator.pop() - some_object = SomeClass() - lazy_string = f('{SomeClass()}') assert len(accumulator) == 0 @@ -95,7 +93,7 @@ def __str__(self): def test_not_lazy(): - number = 5 + number = 5 # noqa: F841 assert type(f('kek', lazy=False)) is str assert type(f('{number}', lazy=False)) is str @@ -269,7 +267,7 @@ def test_genexprs(): def test_not_lazy_mode(): - number = 33 + number = 33 # noqa: F841 assert f('kek', lazy=False) == 'kek' assert f('kek {number}', lazy=False) == 'kek 33' From 11e64ac3df8f66405a9f43cb802abecbdfdc2f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 19:52:13 +0300 Subject: [PATCH 15/52] 1 more assert in a test --- tests/test_init.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_init.py b/tests/test_init.py index ee8273c..7a6bc75 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -17,7 +17,7 @@ def test_basic(): def test_basic_capturing_variables(): - kek = 'kek' + kek = 'kek' # noqa: F841 assert f('{kek}') == 'kek' assert f('{kek}') != 'lol' @@ -184,6 +184,7 @@ def function(): return function_2() assert function() == '{0}'.format(5) + assert function() == function_2() def test_builtins(): From c48976019e473b6c3ba4e6fa2d5c7ee4c41b5b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 19:55:40 +0300 Subject: [PATCH 16/52] more asserts --- tests/test_init.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index 7a6bc75..e063400 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -174,17 +174,18 @@ def function(): return function_2() assert function() == '{0}'.format(5) + assert function() == function_2() # comparing with original interpreter behavior: - def function_2(): + def function_3(): return '{0}'.format(kek) - def function(): + def function_4(): kek = 3 # noqa: F841 return function_2() - assert function() == '{0}'.format(5) - assert function() == function_2() + assert function_3() == '{0}'.format(5) + assert function_3() == function_4() def test_builtins(): From f29319e1318e1d44980fcda600f6b008137f6abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Apr 2025 20:06:45 +0300 Subject: [PATCH 17/52] more test coverage --- tests/test_methods.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_methods.py b/tests/test_methods.py index 824e95c..045edfd 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -319,6 +319,10 @@ def test_startswith(): assert f('kek').startswith(f('')) assert not f('kek').startswith(f('pe')) + assert f('kek').startswith(('k', 'e')) + assert f('kek').startswith(('',)) + assert not f('kek').startswith(('p', 'e')) + def test_endswith(): # str references From 59354ffecd70f6be0bda6ea2e21e4667c77dac96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sun, 13 Apr 2025 01:20:41 +0300 Subject: [PATCH 18/52] bugfix for the .startswith() method --- f/lazy_string.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/f/lazy_string.py b/f/lazy_string.py index 80d54cf..b14a31f 100644 --- a/f/lazy_string.py +++ b/f/lazy_string.py @@ -97,7 +97,7 @@ def startswith(self, prefix: Union['LazyString', str, Tuple[Union['LazyString', if isinstance(prefix, type(self)): prefix = prefix.data elif isinstance(prefix, tuple): - prefix = tuple(*(x.data if isinstance(x, type(self)) else x for x in prefix)) + prefix = tuple((x.data if isinstance(x, type(self)) else x for x in prefix)) return self.data.startswith(prefix, *other_args) def endswith(self, suffix: Union['LazyString', str, Tuple[Union['LazyString', str], ...]], *other_args: int) -> bool: From 90fb8f5443d98e4ba841ca1c53b612eccf6c89c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sun, 13 Apr 2025 01:24:10 +0300 Subject: [PATCH 19/52] bugfix for the .endswith() method --- f/lazy_string.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/f/lazy_string.py b/f/lazy_string.py index b14a31f..759effa 100644 --- a/f/lazy_string.py +++ b/f/lazy_string.py @@ -104,7 +104,7 @@ def endswith(self, suffix: Union['LazyString', str, Tuple[Union['LazyString', st if isinstance(suffix, type(self)): suffix = suffix.data elif isinstance(suffix, tuple): - suffix = tuple(*(x.data if isinstance(x, type(self)) else x for x in suffix)) + suffix = tuple((x.data if isinstance(x, type(self)) else x for x in suffix)) return self.data.endswith(suffix, *other_args) def index(self, sub: Union['LazyString', str], *other: int) -> int: From 1c7cd530e5810ef724b5672cba342415efbc7a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sun, 13 Apr 2025 01:24:33 +0300 Subject: [PATCH 20/52] more test checks --- tests/test_methods.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_methods.py b/tests/test_methods.py index 045edfd..2cbf995 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -323,6 +323,10 @@ def test_startswith(): assert f('kek').startswith(('',)) assert not f('kek').startswith(('p', 'e')) + assert f('kek').startswith(('k', f('e'))) + assert f('kek').startswith(('',)) + assert not f('kek').startswith((f('p'), 'e')) + def test_endswith(): # str references @@ -338,6 +342,10 @@ def test_endswith(): assert f('kek').endswith(f('')) assert not f('kek').endswith(f('pe')) + assert f('kek').endswith((f('e'), f('k'))) + assert f('kek').endswith((f(''),)) + assert not f('kek').endswith((f('p'), f('e'))) + def test_isdigit(): assert f('888').isdigit() From 00bf2a2fb73396d76b4ecabb2ae7941095e4f53d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sun, 13 Apr 2025 01:34:43 +0300 Subject: [PATCH 21/52] more test-cases --- tests/test_methods.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_methods.py b/tests/test_methods.py index 2cbf995..72e0c98 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -476,6 +476,10 @@ def test_maketrans(): assert str.maketrans('mSa', 'eJo', 'odnght') == f('kek').maketrans(f('mSa'), f('eJo'), f('odnght')) assert str.maketrans('S', 'P') == f('kek').maketrans('S', 'P') assert str.maketrans('S', 'P') == f('kek').maketrans(f('S'), f('P')) + assert str.maketrans({'a': 'b', 'r': 't'}) == f('kek').maketrans({'a': 'b', 'r': 't'}) + assert str.maketrans({'a': 'b', 'r': 't'}) == f('kek').maketrans({f('a'): 'b', f('r'): 't'}) + assert str.maketrans({'a': 'b', 'r': 't'}) == f('kek').maketrans({'a': f('b'), 'r': f('t')}) + assert str.maketrans({'a': 'b', 'r': 't'}) == f('kek').maketrans({f('a'): f('b'), f('r'): f('t')}) def test_partition(): From 90f74c61431b7376c25f9060b9c7091d8227e947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sun, 13 Apr 2025 01:36:42 +0300 Subject: [PATCH 22/52] the LoC badge in the readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 52aee15..b8b1923 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Downloads](https://static.pepy.tech/badge/fazy/month)](https://pepy.tech/project/fazy) [![Downloads](https://static.pepy.tech/badge/fazy)](https://pepy.tech/project/fazy) [![codecov](https://codecov.io/gh/pomponchik/fazy/branch/main/graph/badge.svg)](https://codecov.io/gh/pomponchik/fazy) +[![Lines of code](https://sloc.xyz/github/pomponchik/fazy/?category=code)](https://github.com/boyter/scc/) [![Hits-of-Code](https://hitsofcode.com/github/pomponchik/fazy?branch=main)](https://hitsofcode.com/github/pomponchik/fazy/view?branch=main) [![Tests](https://github.com/pomponchik/fazy/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/pomponchik/fazy/actions/workflows/tests_and_coverage.yml) [![PyPI version](https://badge.fury.io/py/fazy.svg)](https://badge.fury.io/py/fazy) From a75ea144c60c2ac380cba5ac814e45ef500e9891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 00:46:35 +0300 Subject: [PATCH 23/52] Update pytest and ruff to newer versions --- requirements_dev.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 7d8c6a1..d1c3e44 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,8 +1,8 @@ -pytest==7.4.3 +pytest==8.0.2 coverage==7.6.1 wheel==0.40.0 twine==6.1.0 build==1.2.2.post1 -ruff==0.9.9 +ruff==0.14.6 mypy==1.14.1 mutmut==3.2.3 From ad97606a14c73879fe66d54f8139e3c8c0c84139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 00:47:34 +0300 Subject: [PATCH 24/52] Configure Ruff linter with custom rules and single quote style --- .ruff.toml | 1 - pyproject.toml | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 .ruff.toml diff --git a/.ruff.toml b/.ruff.toml deleted file mode 100644 index ec5b9a4..0000000 --- a/.ruff.toml +++ /dev/null @@ -1 +0,0 @@ -ignore = ['E501', 'E712'] diff --git a/pyproject.toml b/pyproject.toml index 79cfa7a..512e4c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,10 @@ classifiers = [ [tool.setuptools.package-data] "fazy" = ["py.typed"] +[tool.ruff] +lint.ignore = ['E501', 'E712'] +lint.select = ["ERA001", "YTT", "ASYNC", "BLE", "B", "A", "COM", "INP", "PIE", "T20", "PT", "RSE", "RET", "SIM", "SLOT", "TID252", "ARG", "PTH", "I", "C90", "N", "E", "W", "D201", "D202", "D419", "F", "PL", "PLE", "PLR", "PLW", "RUF", "TRY201", "TRY400", "TRY401"] +format.quote-style = "single" [tool.mutmut] paths_to_mutate="f" From e03558d243990bcf17bbd64612375ae483af9ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 01:25:58 +0300 Subject: [PATCH 25/52] Fix lazy string initialization and update tests for compatibility --- f/__init__.py | 2 +- f/chain_unit.py | 2 +- f/lazy_string.py | 12 +++---- f/proxy_module.py | 10 +++--- pyproject.toml | 2 +- tests/test_dunders.py | 81 ++++++++++++++++++++----------------------- tests/test_init.py | 13 ++++--- tests/test_methods.py | 21 +++++------ tests/test_skipped.py | 8 ++--- 9 files changed, 73 insertions(+), 78 deletions(-) diff --git a/f/__init__.py b/f/__init__.py index 4d394e3..8f31009 100644 --- a/f/__init__.py +++ b/f/__init__.py @@ -1,5 +1,5 @@ import sys -from f.proxy_module import ProxyModule as ProxyModule +from f.proxy_module import ProxyModule as ProxyModule sys.modules[__name__].__class__ = ProxyModule diff --git a/f/chain_unit.py b/f/chain_unit.py index a7cff9b..0866066 100644 --- a/f/chain_unit.py +++ b/f/chain_unit.py @@ -2,7 +2,7 @@ class ChainUnit: - def __init__(self, base: str, appendix: Optional[str] = None, lazy: bool = True) -> None: + def __init__(self, base: str, appendix: Optional[str] = None) -> None: self.base = base self.appendix = appendix if self.appendix is not None: diff --git a/f/lazy_string.py b/f/lazy_string.py index 759effa..ee183aa 100644 --- a/f/lazy_string.py +++ b/f/lazy_string.py @@ -1,11 +1,11 @@ from collections import UserString -from typing import Union, List, Dict, Tuple, Iterable, Callable, Optional, Any +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union from f.chain_unit import ChainUnit class LazyString(UserString, str): # type: ignore[misc] - __slots__ = ('units', 'local_locals', 'local_globals', 'local_nonlocals', 'lazy', 'result') + __slots__ = ('lazy', 'local_globals', 'local_locals', 'local_nonlocals', 'result', 'units') def __init__(self, units: List[ChainUnit], local_locals: Dict[str, Any], local_globals: Dict[str, Any], local_nonlocals: Dict[str, Any], lazy: bool) -> None: self.units: List[ChainUnit] = units @@ -15,7 +15,7 @@ def __init__(self, units: List[ChainUnit], local_locals: Dict[str, Any], local_g self.lazy: bool = lazy self.result: Optional[str] = None - def __new__(cls, *args: Any, **kwargs: Any) -> 'LazyString': + def __new__(cls, *args: Any, **kwargs: Any) -> 'LazyString': # noqa: ARG004 return str.__new__(cls) def __add__(self, other: Union['LazyString', str]) -> str: @@ -57,7 +57,7 @@ def __reduce__(self) -> Union[str, Tuple[Callable[[], Any], Tuple[Any, ...], Ite def __setattr__(self, name: str, value: Any) -> None: if name not in type(self).__slots__: raise AttributeError( - "'{0}' object has no attribute '{1}'".format(type(self).__name__, name) + "'{0}' object has no attribute '{1}'".format(type(self).__name__, name), ) object.__setattr__(self, name, value) @@ -176,8 +176,8 @@ def maketrans(x: Union[Dict[Union[int, str, UserString], Optional[Union[int, str first_item = {} for key, value in x.items(): - key = key.data if isinstance(key, UserString) else key - value = value.data if isinstance(value, UserString) else value + key = key.data if isinstance(key, UserString) else key # noqa: PLW2901 + value = value.data if isinstance(value, UserString) else value # noqa: PLW2901 first_item[key] = value return str.maketrans(first_item, *converted_others) # type: ignore[arg-type] diff --git a/f/proxy_module.py b/f/proxy_module.py index 2bd355a..dd83287 100644 --- a/f/proxy_module.py +++ b/f/proxy_module.py @@ -1,9 +1,9 @@ import gc -import sys import inspect +import sys from string import Formatter from types import CodeType, FrameType -from typing import Protocol, Iterable, Optional, Union, Sized, Dict, Type, Any +from typing import Any, Dict, Iterable, Optional, Protocol, Sized, Type, Union from f.chain_unit import ChainUnit from f.lazy_string import LazyString @@ -27,7 +27,7 @@ def __call__(self, string: Union[LazyString, str], lazy: bool = True, closures: {**base_frame.f_globals}, self.sum_of_nonlocals( base_frame.f_back, - self.get_qualname(base_frame.f_code, code_line=base_frame.f_lineno), + self.get_qualname(base_frame.f_code), closures, ), lazy, @@ -51,7 +51,7 @@ def sum_of_nonlocals(self, first_frame: Optional[FrameType], base_qualname: Opti while first_frame is not None: code = first_frame.f_code - qualname = self.get_qualname(code, code_line=0) + qualname = self.get_qualname(code) if qualname is not None: if self.startswith(base_qualname.split('.'), qualname.split('.')): all_locals.append(first_frame.f_locals) @@ -68,7 +68,7 @@ def sum_of_nonlocals(self, first_frame: Optional[FrameType], base_qualname: Opti return result @classmethod - def get_qualname(cls: Type['ProxyModule'], code: CodeType, code_line: int) -> Optional[str]: + def get_qualname(cls: Type['ProxyModule'], code: CodeType) -> Optional[str]: functions = [] for function in gc.get_referrers(code): diff --git a/pyproject.toml b/pyproject.toml index 512e4c2..e463ca9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ "fazy" = ["py.typed"] [tool.ruff] -lint.ignore = ['E501', 'E712'] +lint.ignore = ['E501', 'E712', 'PTH123', 'PTH118', 'PLR2004', 'PTH107', 'SIM105', 'SIM102', 'RET503'] lint.select = ["ERA001", "YTT", "ASYNC", "BLE", "B", "A", "COM", "INP", "PIE", "T20", "PT", "RSE", "RET", "SIM", "SLOT", "TID252", "ARG", "PTH", "I", "C90", "N", "E", "W", "D201", "D202", "D419", "F", "PL", "PLE", "PLR", "PLW", "RUF", "TRY201", "TRY400", "TRY401"] format.quote-style = "single" diff --git a/tests/test_dunders.py b/tests/test_dunders.py index 55c733d..41641ef 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -1,6 +1,7 @@ import sys import pytest +from full_match import match import f @@ -22,17 +23,16 @@ def test_dunder_repr(): def test_dunder_eq(): lazy_string = f('kek') - assert lazy_string == lazy_string + assert lazy_string == lazy_string # noqa: PLR0124 assert f('kek') == f('kek') assert lazy_string == 'kek' assert lazy_string != 'no kek' - assert 'kek' == lazy_string - assert 'no kek' != lazy_string + assert lazy_string == 'kek' + assert lazy_string != 'no kek' assert f('e') == 'e' - assert 'e' == f('e') def test_dunder_add_and_radd(): @@ -45,13 +45,8 @@ def test_dunder_add_and_radd(): assert f('lol') + f('kek') != f('not lolkek') - with pytest.raises(TypeError): - f('lol') + 5 - - try: + with pytest.raises(TypeError, match=match('can only concatenate str (not "int") to str')): f('lol') + 5 - except TypeError as e: - assert str(e) == 'can only concatenate str (not "int") to str' def test_dunder_contains(): @@ -99,93 +94,93 @@ def test_dunder_getnewargs(): def test_dunder_ge(): - assert '1234' >= '123' + assert '1234' >= '123' # noqa: PLR0133 assert f('1234') >= '123' assert f('1234') >= f('123') - assert '1234' >= f('123') + assert f('123') <= '1234' - assert '1234' >= '1234' + assert '1234' >= '1234' # noqa: PLR0133 assert f('1234') >= '1234' assert f('1234') >= f('1234') - assert '1234' >= f('1234') + assert f('1234') <= '1234' - assert not ('1234' >= '12345') + assert not ('1234' >= '12345') # noqa: PLR0133 assert not (f('1234') >= '12345') assert not (f('1234') >= f('12345')) - assert not ('1234' >= f('12345')) + assert not (f('12345') <= '1234') with pytest.raises(TypeError): - f('1234') >= 12345 + f('1234') >= 12345 # noqa: B015 def test_dunder_gt(): - assert '1234' > '123' + assert '1234' > '123' # noqa: PLR0133 assert f('1234') > '123' assert f('1234') > f('123') - assert '1234' > f('123') + assert f('123') < '1234' - assert not ('1234' > '1234') + assert not ('1234' > '1234') # noqa: PLR0133 assert not (f('1234') > '1234') assert not (f('1234') > f('1234')) - assert not ('1234' > f('1234')) + assert not (f('1234') < '1234') - assert not ('1234' > '12345') + assert not ('1234' > '12345') # noqa: PLR0133 assert not (f('1234') > '12345') assert not (f('1234') > f('12345')) - assert not ('1234' > f('12345')) + assert not (f('12345') < '1234') with pytest.raises(TypeError): - f('1234') > 12345 + f('1234') > 12345 # noqa: B015 def test_dunder_le(): - assert '123' <= '1234' + assert '123' <= '1234' # noqa: PLR0133 assert f('123') <= '1234' assert f('123') <= f('1234') - assert '123' <= f('1234') + assert f('1234') >= '123' - assert '1234' <= '1234' + assert '1234' <= '1234' # noqa: PLR0133 assert f('1234') <= '1234' assert f('1234') <= f('1234') - assert '1234' <= f('1234') + assert f('1234') >= '1234' - assert not ('12345' <= '1234') + assert not ('12345' <= '1234') # noqa: PLR0133 assert not (f('12345') <= '1234') assert not (f('12345') <= f('1234')) - assert not ('12345' <= f('1234')) + assert not (f('1234') >= '12345') with pytest.raises(TypeError): - f('1234') <= 12345 + f('1234') <= 12345 # noqa: B015 def test_dunder_lt(): - assert '123' < '1234' + assert '123' < '1234' # noqa: PLR0133 assert f('123') < '1234' assert f('123') < f('1234') - assert '123' < f('1234') + assert f('1234') > '123' - assert not ('12345' < '1234') + assert not ('12345' < '1234') # noqa: PLR0133 assert not (f('12345') < '1234') assert not (f('12345') < f('1234')) - assert not ('12345' < f('1234')) + assert not (f('1234') > '12345') with pytest.raises(TypeError): - f('1234') < 12345 + f('1234') < 12345 # noqa: B015 def test_dunder_ne(): - assert '123' != '1234' + assert '123' != '1234' # noqa: PLR0133 assert f('123') != '1234' assert f('123') != f('1234') - assert '123' != f('1234') + assert f('1234') != '123' - assert not ('1234' != '1234') - assert not (f('1234') != '1234') - assert not (f('1234') != f('1234')) - assert not ('1234' != f('1234')) + assert not ('1234' != '1234') # noqa: PLR0133, SIM202 + assert not (f('1234') != '1234') # noqa: SIM202 + assert not (f('1234') != f('1234')) # noqa: SIM202 + assert not (f('1234') != '1234') # noqa: SIM202 assert f('1234') != 1234 - assert 1234 != f('1234') + assert f('1234') != 1234 def test_dunder_hash(): diff --git a/tests/test_init.py b/tests/test_init.py index e063400..478d973 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,13 +1,12 @@ -import os import logging -from io import StringIO +import os from contextlib import redirect_stdout +from io import StringIO import pytest import f - GLOBAL_VARIABLE = 'kek' @@ -29,7 +28,7 @@ def test_basic_capturing_global_variables(): def test_globals_and_locals_intersection(): - GLOBAL_VARIABLE = 'lol' + GLOBAL_VARIABLE = 'lol' # noqa: N806 assert f('{GLOBAL_VARIABLE}') == 'lol' assert f('{GLOBAL_VARIABLE}') == '{0}'.format(GLOBAL_VARIABLE) @@ -75,7 +74,7 @@ def __str__(self): return 'kek' assert f('{SomeClass()}') == 'kek' - len(accumulator) == 1 + assert len(accumulator) == 1 accumulator.pop() @@ -164,7 +163,7 @@ def function(): def test_read_nonlocal_variable_nested(): - kek = 5 # noqa: F841 + kek = 5 def function_2(): return f('{kek}') @@ -201,7 +200,7 @@ def test_modules_startswith(): def test_print(): with redirect_stdout(StringIO()) as context: - print(f('kek')) + print(f('kek')) # noqa: T201 assert context.getvalue() == 'kek\n' diff --git a/tests/test_methods.py b/tests/test_methods.py index 72e0c98..6a8623e 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -1,6 +1,7 @@ import sys import pytest +from full_match import match import f @@ -91,8 +92,8 @@ def test_index(): # str references assert 'kek'.index('k') == 0 assert 'kek'.index('e') == 1 - with pytest.raises(ValueError): - 'kek'.index('p') == -1 + with pytest.raises(ValueError, match=match('substring not found')): + 'kek'.index('p') assert f('kek').index('k') == 0 assert f('kek').index(f('k')) == 0 @@ -100,11 +101,11 @@ def test_index(): assert f('kek').index('e') == 1 assert f('kek').index(f('e')) == 1 - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=match('substring not found')): f('kek').index('p') - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=match('substring not found')): f('kek').index(f('p')) - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=match('must be str, not int')): f('kek').index(0) @@ -112,8 +113,8 @@ def test_rindex(): # str references assert 'kek'.rindex('k') == 2 assert 'kek'.rindex('e') == 1 - with pytest.raises(ValueError): - 'kek'.rindex('p') == -1 + with pytest.raises(ValueError, match=match('substring not found')): + 'kek'.rindex('p') assert f('kek').rindex('k') == 2 assert f('kek').rindex(f('k')) == 2 @@ -121,11 +122,11 @@ def test_rindex(): assert f('kek').rindex('e') == 1 assert f('kek').rindex(f('e')) == 1 - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=match('substring not found')): f('kek').rindex('p') - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=match('substring not found')): f('kek').rindex(f('p')) - with pytest.raises(TypeError): + with pytest.raises(TypeError, match=match('must be str, not int')): f('kek').rindex(0) diff --git a/tests/test_skipped.py b/tests/test_skipped.py index 722a8c6..45eaaf7 100644 --- a/tests/test_skipped.py +++ b/tests/test_skipped.py @@ -1,13 +1,13 @@ import os -import sys import pickle -from tempfile import TemporaryDirectory +import sys from string import Formatter - -import f +from tempfile import TemporaryDirectory import pytest +import f + @pytest.mark.skip(reason='It is impossible to do it.') def test_count_reverse(): From 3b415c5bb47c552e70ea9287485f2dd91916bd15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 01:28:32 +0300 Subject: [PATCH 26/52] Update workflow actions to latest versions --- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/tests_and_coverage.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index de91ff7..17b4ad5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,10 +12,10 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd1689b..4d019f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,10 +15,10 @@ jobs: # IMPORTANT: this permission is mandatory for trusted publishing id-token: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml index 8f4685d..b63631e 100644 --- a/.github/workflows/tests_and_coverage.yml +++ b/.github/workflows/tests_and_coverage.yml @@ -13,9 +13,9 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} From c357498b5ca77f3555f9c9398f112af34385baed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 01:29:55 +0300 Subject: [PATCH 27/52] Remove lazy flag from ChainUnit creation in proxy module --- f/proxy_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/f/proxy_module.py b/f/proxy_module.py index dd83287..de1b99d 100644 --- a/f/proxy_module.py +++ b/f/proxy_module.py @@ -22,7 +22,7 @@ def __call__(self, string: Union[LazyString, str], lazy: bool = True, closures: base_frame = inspect.stack(0)[1].frame result = LazyString( - [ChainUnit(base=x[0], appendix=x[1], lazy=lazy) for x in Formatter().parse(string)], + [ChainUnit(base=x[0], appendix=x[1]) for x in Formatter().parse(string)], {**base_frame.f_locals}, {**base_frame.f_globals}, self.sum_of_nonlocals( From eaf4ed085c822bc42bf005d40b3d1d79ccf7c5a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 01:30:28 +0300 Subject: [PATCH 28/52] Add full_match to development dependencies --- requirements_dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_dev.txt b/requirements_dev.txt index d1c3e44..ceaebc4 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -6,3 +6,4 @@ build==1.2.2.post1 ruff==0.14.6 mypy==1.14.1 mutmut==3.2.3 +full_match==0.0.3 From 981729874e6f4b8bbf00fd52590c270a76527ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 01:33:33 +0300 Subject: [PATCH 29/52] Fix TypeError assertion for rindex with int input --- tests/test_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_methods.py b/tests/test_methods.py index 6a8623e..52bc736 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -126,7 +126,7 @@ def test_rindex(): f('kek').rindex('p') with pytest.raises(ValueError, match=match('substring not found')): f('kek').rindex(f('p')) - with pytest.raises(TypeError, match=match('must be str, not int')): + with pytest.raises(TypeError): f('kek').rindex(0) From 2aa56dbd472af18f8346c9d1091a46ca61e19459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 01:33:52 +0300 Subject: [PATCH 30/52] Fix TypeError message in test_index --- tests/test_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_methods.py b/tests/test_methods.py index 52bc736..a017131 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -105,7 +105,7 @@ def test_index(): f('kek').index('p') with pytest.raises(ValueError, match=match('substring not found')): f('kek').index(f('p')) - with pytest.raises(TypeError, match=match('must be str, not int')): + with pytest.raises(TypeError): f('kek').index(0) From 567f53b4f886af30cea46d625b2a6d66bdf4cdc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 01:46:55 +0300 Subject: [PATCH 31/52] One more test --- tests/test_init.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_init.py b/tests/test_init.py index 478d973..dcd61d8 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -296,3 +296,10 @@ def wrapped(): with pytest.raises(NameError): assert wrapper()() + + +def test_just_simple_exec(): + globals_for_module = {} + exec('import f; a = f("kek")', globals_for_module) + + assert globals_for_module['a'] == f('kek') From 8e9974ae6cf3ab2a23935b2f3c9c06e00940694a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 01:55:42 +0300 Subject: [PATCH 32/52] Add pragma to suppress coverage warning for condition --- f/proxy_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/f/proxy_module.py b/f/proxy_module.py index de1b99d..72601db 100644 --- a/f/proxy_module.py +++ b/f/proxy_module.py @@ -52,7 +52,7 @@ def sum_of_nonlocals(self, first_frame: Optional[FrameType], base_qualname: Opti code = first_frame.f_code qualname = self.get_qualname(code) - if qualname is not None: + if qualname is not None: # pragma: no cover if self.startswith(base_qualname.split('.'), qualname.split('.')): all_locals.append(first_frame.f_locals) From d327b428a4196ee65d169acd3d297f4f4cebc600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 01:56:14 +0300 Subject: [PATCH 33/52] Add pragma: no cover to function return guard --- f/proxy_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/f/proxy_module.py b/f/proxy_module.py index 72601db..f8c67f0 100644 --- a/f/proxy_module.py +++ b/f/proxy_module.py @@ -80,7 +80,7 @@ def get_qualname(cls: Type['ProxyModule'], code: CodeType) -> Optional[str]: if maybe_code is not None: functions.append(function) - if functions: + if functions: # pragma: no cover function = functions[0] return function.__qualname__ # type: ignore[no-any-return] From 0b33a5d8763c03b5f9e62e806792ab926dbaadb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 01:59:05 +0300 Subject: [PATCH 34/52] Update codecov badge to coveralls badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b8b1923..e233af4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Downloads](https://static.pepy.tech/badge/fazy/month)](https://pepy.tech/project/fazy) [![Downloads](https://static.pepy.tech/badge/fazy)](https://pepy.tech/project/fazy) -[![codecov](https://codecov.io/gh/pomponchik/fazy/branch/main/graph/badge.svg)](https://codecov.io/gh/pomponchik/fazy) +[![Coverage Status](https://coveralls.io/repos/github/pomponchik/fazy/badge.svg?branch=main)](https://coveralls.io/github/pomponchik/fazy?branch=main) [![Lines of code](https://sloc.xyz/github/pomponchik/fazy/?category=code)](https://github.com/boyter/scc/) [![Hits-of-Code](https://hitsofcode.com/github/pomponchik/fazy?branch=main)](https://hitsofcode.com/github/pomponchik/fazy/view?branch=main) [![Tests](https://github.com/pomponchik/fazy/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/pomponchik/fazy/actions/workflows/tests_and_coverage.yml) From 44de57df04a7026f918a106c772f2e186fb249bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 01:59:19 +0300 Subject: [PATCH 35/52] Mark removeprefix and removesuffix as covered by pragma no cover --- f/lazy_string.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/f/lazy_string.py b/f/lazy_string.py index ee183aa..a8178c9 100644 --- a/f/lazy_string.py +++ b/f/lazy_string.py @@ -134,12 +134,12 @@ def casefold(self) -> str: def expandtabs(self, tabsize: int = 8) -> str: return self.data.expandtabs(tabsize) - def removeprefix(self, prefix: Union['LazyString', str]) -> str: + def removeprefix(self, prefix: Union['LazyString', str]) -> str: # pragma: no cover if isinstance(prefix, type(self)): prefix = prefix.data return self.data.removeprefix(prefix) # type: ignore[attr-defined, no-any-return, unused-ignore] - def removesuffix(self, suffix: Union['LazyString', str]) -> str: + def removesuffix(self, suffix: Union['LazyString', str]) -> str: # pragma: no cover if isinstance(suffix, type(self)): suffix = suffix.data return self.data.removesuffix(suffix) # type: ignore[attr-defined, no-any-return, unused-ignore] From 87653e5c53cd419a6d0c42dbe87a355344bee603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 02:00:23 +0300 Subject: [PATCH 36/52] Add Python 3.14 to classifiers --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index e463ca9..3ee6c38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', From 497518631f656cab757c3e47b240b149e98c6346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 02:05:18 +0300 Subject: [PATCH 37/52] Enable debug print in test_init.py --- tests/test_init.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_init.py b/tests/test_init.py index dcd61d8..bc67491 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -251,6 +251,7 @@ def test_logging_to_file(): with open(file_name, 'r') as file: content = file.read() + print(repr(content)) assert content == 'kek\n' try: From acfd0010792d3f4411e4ac285547c8ede2cec138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 02:09:04 +0300 Subject: [PATCH 38/52] Add test_logging with skip marker --- tests/test_init.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_init.py b/tests/test_init.py index bc67491..d67f5d0 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -222,6 +222,7 @@ def test_lazy_syntax_error(): str(f('{a..}')) +@pytest.mark.skip def test_logging(): class ListHandler(logging.Handler): def __init__(self, log_list): From 4abb2493f9b0872f14bf72654e89f1017062d485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 02:43:24 +0300 Subject: [PATCH 39/52] Add logging file content print to test ```python with open(file_name, 'r') as file: content = file.read() print(repr(content)) ``` --- tests/test_init.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_init.py b/tests/test_init.py index d67f5d0..6b6b715 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -248,6 +248,10 @@ def test_logging_to_file(): file_name = os.path.join('tests', 'data', 'file.log') logging.root.addHandler(logging.FileHandler(file_name)) + with open(file_name, 'r') as file: + content = file.read() + print(repr(content)) + logging.error(f('kek')) with open(file_name, 'r') as file: From 30ccb9a73548dda829afd57581f318b2a388bbc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 02:46:48 +0300 Subject: [PATCH 40/52] Fix test_logging to read and print log file content --- tests/test_init.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_init.py b/tests/test_init.py index 6b6b715..6226ecd 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -222,7 +222,6 @@ def test_lazy_syntax_error(): str(f('{a..}')) -@pytest.mark.skip def test_logging(): class ListHandler(logging.Handler): def __init__(self, log_list): @@ -245,6 +244,10 @@ def emit(self, record): def test_logging_to_file(): + with open(file_name, 'r') as file: + content = file.read() + print(repr(content)) + file_name = os.path.join('tests', 'data', 'file.log') logging.root.addHandler(logging.FileHandler(file_name)) From b0a927f4483571b0ea6d51943ae510ff153763df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 02:48:13 +0300 Subject: [PATCH 41/52] Add file logging handler to test_logging_to_file --- tests/test_init.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_init.py b/tests/test_init.py index 6226ecd..385d39d 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -244,11 +244,12 @@ def emit(self, record): def test_logging_to_file(): + file_name = os.path.join('tests', 'data', 'file.log') with open(file_name, 'r') as file: content = file.read() print(repr(content)) - file_name = os.path.join('tests', 'data', 'file.log') + logging.root.addHandler(logging.FileHandler(file_name)) with open(file_name, 'r') as file: From 02a9c75f4441d9ab84b9d73b174b2863a3477a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 02:52:33 +0300 Subject: [PATCH 42/52] Add debug prints to track handler state during test_logging_to_file --- tests/test_init.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index 385d39d..25d474a 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -244,20 +244,20 @@ def emit(self, record): def test_logging_to_file(): + print('handlers before', logging.root.handlers) file_name = os.path.join('tests', 'data', 'file.log') - with open(file_name, 'r') as file: - content = file.read() - print(repr(content)) - - logging.root.addHandler(logging.FileHandler(file_name)) + print('handlers after adding a handler', logging.root.handlers) + with open(file_name, 'r') as file: content = file.read() print(repr(content)) logging.error(f('kek')) + print('handlers after logging', logging.root.handlers) + with open(file_name, 'r') as file: content = file.read() print(repr(content)) From 3f0c38782a34f984152605a5b19ab72c319dcb93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 03:04:50 +0300 Subject: [PATCH 43/52] Use temporary directory for logging file tests --- tests/test_init.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index 25d474a..bc9bf09 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -244,29 +244,33 @@ def emit(self, record): def test_logging_to_file(): + from tempfile import TemporaryDirectory + + print('handlers before', logging.root.handlers) - file_name = os.path.join('tests', 'data', 'file.log') - logging.root.addHandler(logging.FileHandler(file_name)) + with TemporaryDirectory() as path: + file_name = os.path.join(path, 'file.log') + logging.root.addHandler(logging.FileHandler(file_name)) - print('handlers after adding a handler', logging.root.handlers) + print('handlers after adding a handler', logging.root.handlers) - with open(file_name, 'r') as file: - content = file.read() - print(repr(content)) + with open(file_name, 'r') as file: + content = file.read() + print(repr(content)) - logging.error(f('kek')) + logging.error(f('kek')) - print('handlers after logging', logging.root.handlers) + print('handlers after logging', logging.root.handlers) - with open(file_name, 'r') as file: - content = file.read() - print(repr(content)) - assert content == 'kek\n' + with open(file_name, 'r') as file: + content = file.read() + print(repr(content)) + assert content == 'kek\n' - try: - os.remove(file_name) - except PermissionError: # windows oddities - pass + try: + os.remove(file_name) + except PermissionError: # windows oddities + pass def test_list_comprehension(): From 186e176c16a4371376de09b4cf362f8e72a2e072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 03:09:51 +0300 Subject: [PATCH 44/52] Remove try-except block for os.remove in test_logging_to_file --- tests/test_init.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index bc9bf09..85e5ba6 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -267,11 +267,6 @@ def test_logging_to_file(): print(repr(content)) assert content == 'kek\n' - try: - os.remove(file_name) - except PermissionError: # windows oddities - pass - def test_list_comprehension(): assert [f('{x}') for x in range(5)] == ['0', '1', '2', '3', '4'] From eb63e6c975473e152db2205fb5f192ceed4a643f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 03:14:16 +0300 Subject: [PATCH 45/52] Refactor logging handler addition to use variable assignment --- tests/test_init.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_init.py b/tests/test_init.py index 85e5ba6..fccdf0f 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -250,7 +250,8 @@ def test_logging_to_file(): print('handlers before', logging.root.handlers) with TemporaryDirectory() as path: file_name = os.path.join(path, 'file.log') - logging.root.addHandler(logging.FileHandler(file_name)) + handler = logging.FileHandler(file_name) + logging.root.addHandler(handler) print('handlers after adding a handler', logging.root.handlers) @@ -267,6 +268,8 @@ def test_logging_to_file(): print(repr(content)) assert content == 'kek\n' + logging.root.handlers.remove(handler) + def test_list_comprehension(): assert [f('{x}') for x in range(5)] == ['0', '1', '2', '3', '4'] From 7070d5380a254022a8a0062e4f79fa61b85978fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 03:20:08 +0300 Subject: [PATCH 46/52] Fix file cleanup in test_logging_to_file --- tests/test_init.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_init.py b/tests/test_init.py index fccdf0f..a535c97 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -270,6 +270,11 @@ def test_logging_to_file(): logging.root.handlers.remove(handler) + try: + os.remove(file_name) + except PermissionError: # windows oddities + pass + def test_list_comprehension(): assert [f('{x}') for x in range(5)] == ['0', '1', '2', '3', '4'] From 5f6561fa04ed5613c4825452b07cc09a3213cc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 03:31:07 +0300 Subject: [PATCH 47/52] Remove test_logging_to_file function from tests --- tests/test_init.py | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index a535c97..a9fcfaf 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -243,39 +243,6 @@ def emit(self, record): assert type(lst[0].message) is str -def test_logging_to_file(): - from tempfile import TemporaryDirectory - - - print('handlers before', logging.root.handlers) - with TemporaryDirectory() as path: - file_name = os.path.join(path, 'file.log') - handler = logging.FileHandler(file_name) - logging.root.addHandler(handler) - - print('handlers after adding a handler', logging.root.handlers) - - with open(file_name, 'r') as file: - content = file.read() - print(repr(content)) - - logging.error(f('kek')) - - print('handlers after logging', logging.root.handlers) - - with open(file_name, 'r') as file: - content = file.read() - print(repr(content)) - assert content == 'kek\n' - - logging.root.handlers.remove(handler) - - try: - os.remove(file_name) - except PermissionError: # windows oddities - pass - - def test_list_comprehension(): assert [f('{x}') for x in range(5)] == ['0', '1', '2', '3', '4'] From 42ff62f3954fc3bfcb04f4aecc614ce394d5e587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 03:38:22 +0300 Subject: [PATCH 48/52] Remove unused `os` import --- tests/test_init.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_init.py b/tests/test_init.py index a9fcfaf..20bbb34 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,5 +1,4 @@ import logging -import os from contextlib import redirect_stdout from io import StringIO From 8a826cae32e0a1dc61670c81c2e1285dcb3b610b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 03:40:25 +0300 Subject: [PATCH 49/52] Add Python 3.14 and 3.14t to test matrix --- .github/workflows/lint.yml | 2 +- .github/workflows/tests_and_coverage.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 17b4ad5..300cc5f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml index b63631e..01fc98d 100644 --- a/.github/workflows/tests_and_coverage.yml +++ b/.github/workflows/tests_and_coverage.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t'] steps: - uses: actions/checkout@v4 From 8a14907f7cb4f4568a03ded08e256bd9e24e63b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 03:43:55 +0300 Subject: [PATCH 50/52] New version tag --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3ee6c38..666beff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta' [project] name = 'fazy' -version = '0.0.10' +version = '0.0.11' authors = [ { name='Evgeniy Blinov', email='zheni-b@yandex.ru' }, ] From 36173739743e4411689ee9f262b0983d7bfd5491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 03:45:46 +0300 Subject: [PATCH 51/52] Set up Python using actions/setup-python@v5 --- .github/workflows/release.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4d019f5..f61d6c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,10 +17,8 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - name: Install dependencies shell: bash From dc1fdbf18216563c7ab6a61898b405be0c9edb5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Nov 2025 04:07:26 +0300 Subject: [PATCH 52/52] Update test workflow to use Coveralls and cache pip dependencies --- .github/workflows/tests_and_coverage.yml | 73 ++++++++++++++---------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml index 01fc98d..30f54aa 100644 --- a/.github/workflows/tests_and_coverage.yml +++ b/.github/workflows/tests_and_coverage.yml @@ -1,44 +1,55 @@ name: Tests -on: - push +on: push jobs: build: - runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t'] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: python-version: ${{ matrix.python-version }} - - name: Install the library - shell: bash - run: pip install . - - - name: Install dependencies - shell: bash - run: pip install -r requirements_dev.txt - - - name: Run tests and show coverage on the command line - run: coverage run --source=f --omit="*tests*" -m pytest --cache-clear && coverage report -m --fail-under=100 - - - name: Upload reports to codecov - env: - CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} - if: runner.os == 'Linux' - run: | - curl -Os https://uploader.codecov.io/latest/linux/codecov - find . -iregex "codecov.*" - chmod +x codecov - ./codecov -t ${CODECOV_TOKEN} - - - name: Run tests and show the branch coverage on the command line - run: coverage run --branch --source=f --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m --fail-under=100 + - name: Install the library + shell: bash + run: pip install . + + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ github.workflow }}-${{ hashFiles('requirements_dev.txt') }} + restore-keys: | + ${{ runner.os }}-pip-${{ github.workflow }}- + + - name: Install dependencies + shell: bash + run: pip install -r requirements_dev.txt + + - name: Print all libs + shell: bash + run: pip list + + - name: Run tests and show coverage on the command line + run: | + coverage run --source=f --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m --fail-under=100 + coverage xml + + - name: Upload coverage to Coveralls + if: runner.os == 'Linux' + env: + COVERALLS_REPO_TOKEN: ${{secrets.COVERALLS_REPO_TOKEN}} + uses: coverallsapp/github-action@v2 + with: + format: cobertura + file: coverage.xml + + - name: Run tests and show the branch coverage on the command line + run: coverage run --branch --source=f --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m --fail-under=100