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. diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 85c7a79..300cc5f 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', '3.14', '3.14t'] 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 }} @@ -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/release.yml b/.github/workflows/release.yml index fd1689b..f61d6c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,12 +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 - with: - python-version: ${{ matrix.python-version }} + - name: Set up Python + uses: actions/setup-python@v5 - name: Install dependencies shell: bash diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml index c1c1bb4..30f54aa 100644 --- a/.github/workflows/tests_and_coverage.yml +++ b/.github/workflows/tests_and_coverage.yml @@ -1,41 +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.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", "3.14", "3.14t"] steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - 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 - - - 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: 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 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/.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/README.md b/README.md index 52aee15..e233af4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ [![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) [![PyPI version](https://badge.fury.io/py/fazy.svg)](https://badge.fury.io/py/fazy) 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 80d54cf..a8178c9 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) @@ -97,14 +97,14 @@ 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: 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: @@ -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] @@ -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 4830cbc..f8c67f0 100644 --- a/f/proxy_module.py +++ b/f/proxy_module.py @@ -1,14 +1,9 @@ import gc -import sys -import ast import inspect +import sys from string import Formatter from types import CodeType, FrameType -from typing import Iterable, Optional, Union, Sized, Dict, Callable, Type, Any -try: - from typing import Protocol -except ImportError: - from typing_extensions import Protocol # type: ignore[assignment] +from typing import Any, Dict, Iterable, Optional, Protocol, Sized, Type, Union from f.chain_unit import ChainUnit from f.lazy_string import LazyString @@ -20,21 +15,20 @@ 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 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( 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), closures, - safe, ), lazy, ) @@ -43,7 +37,13 @@ def __call__(self, string: Union[LazyString, str], lazy: bool = True, safe: bool return result return result.data - def sum_of_nonlocals(self, first_frame: Optional[FrameType], base_qualname: Optional[str], closures: bool, safe: bool) -> Dict[str, Any]: + 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) -> Dict[str, Any]: if not closures or first_frame is None or base_qualname is None: return {} @@ -51,8 +51,8 @@ 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) - if qualname is not None: + qualname = self.get_qualname(code) + if qualname is not None: # pragma: no cover 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, raise_if_not_literal: bool, code_line: int) -> Optional[str]: + def get_qualname(cls: Type['ProxyModule'], code: CodeType) -> Optional[str]: functions = [] for function in gc.get_referrers(code): @@ -80,67 +80,10 @@ def get_qualname(cls: Type['ProxyModule'], code: CodeType, raise_if_not_literal: if maybe_code is not None: functions.append(function) - if functions: + if functions: # pragma: no cover 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): @@ -151,9 +94,3 @@ def startswith(iterable: SizedAndIterable, second_iterable: SizedAndIterable) -> return False return True - - def __str__(self) -> str: - return 'f' - - def __repr__(self) -> str: - return 'f' diff --git a/pyproject.toml b/pyproject.toml index 8268f4e..666beff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,25 +4,26 @@ 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' }, ] 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', + 'Programming Language :: Python :: 3.14', 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', @@ -31,6 +32,15 @@ classifiers = [ [tool.setuptools.package-data] "fazy" = ["py.typed"] +[tool.ruff] +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" + +[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..ceaebc4 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,7 +1,9 @@ -pytest==7.2.2 -coverage==7.2.2 +pytest==8.0.2 +coverage==7.6.1 wheel==0.40.0 -twine==4.0.2 -build==0.9.0 -ruff==0.0.290 -mypy==1.4.1 +twine==6.1.0 +build==1.2.2.post1 +ruff==0.14.6 +mypy==1.14.1 +mutmut==3.2.3 +full_match==0.0.3 diff --git a/tests/test_dunders.py b/tests/test_dunders.py index a6e0f47..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(): @@ -67,7 +62,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 @@ -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 e0bc8ae..20bbb34 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,14 +1,11 @@ -import os -import sys import logging -from io import StringIO from contextlib import redirect_stdout +from io import StringIO import pytest import f - GLOBAL_VARIABLE = 'kek' @@ -18,7 +15,7 @@ def test_basic(): def test_basic_capturing_variables(): - kek = 'kek' + kek = 'kek' # noqa: F841 assert f('{kek}') == 'kek' assert f('{kek}') != 'lol' @@ -30,7 +27,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) @@ -39,7 +36,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' @@ -76,12 +73,10 @@ def __str__(self): return 'kek' assert f('{SomeClass()}') == 'kek' - len(accumulator) == 1 + assert len(accumulator) == 1 accumulator.pop() - some_object = SomeClass() - lazy_string = f('{SomeClass()}') assert len(accumulator) == 0 @@ -96,7 +91,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 @@ -173,20 +168,22 @@ def function_2(): return f('{kek}') def function(): - kek = 3 + kek = 3 # noqa: F841 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(): - kek = 3 + def function_4(): + kek = 3 # noqa: F841 return function_2() - assert function() == '{0}'.format(5) + assert function_3() == '{0}'.format(5) + assert function_3() == function_4() def test_builtins(): @@ -202,7 +199,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' @@ -245,22 +242,6 @@ def emit(self, record): assert type(lst[0].message) is str -def test_logging_to_file(): - file_name = os.path.join('tests', 'data', 'file.log') - logging.root.addHandler(logging.FileHandler(file_name)) - - logging.error(f('kek')) - - with open(file_name, 'r') as file: - content = file.read() - 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'] @@ -270,7 +251,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' @@ -280,7 +261,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' @@ -288,10 +269,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 @@ -300,41 +281,8 @@ def wrapped(): 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_just_simple_exec(): + globals_for_module = {} + exec('import f; a = f("kek")', globals_for_module) -def test_string_as_variable_when_not_safe_mode(): - string = 'kek' - assert f(string, safe=False) == string + assert globals_for_module['a'] == f('kek') diff --git a/tests/test_methods.py b/tests/test_methods.py index 824e95c..a017131 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,9 +101,9 @@ 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): 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,9 +122,9 @@ 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): f('kek').rindex(0) @@ -319,6 +320,14 @@ 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')) + + assert f('kek').startswith(('k', f('e'))) + assert f('kek').startswith(('',)) + assert not f('kek').startswith((f('p'), 'e')) + def test_endswith(): # str references @@ -334,6 +343,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() @@ -464,6 +477,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(): diff --git a/tests/test_skipped.py b/tests/test_skipped.py index 8e99af1..45eaaf7 100644 --- a/tests/test_skipped.py +++ b/tests/test_skipped.py @@ -1,12 +1,13 @@ -import sys +import os 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():