From 14e4e6deff88df0bfaac1bd3c480fe03b2c753b4 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: Tue, 9 Dec 2025 23:45:51 +0300 Subject: [PATCH 01/15] Update comment syntax examples to use arrows for consistency --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ff931bb..3e87f38 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,12 @@ Many source code analysis tools use comments in a special format to mark it up. In the Python ecosystem, there are many tools dealing with source code: linters, test coverage collection systems, and many others. Many of them use special comments, and as a rule, the style of these comments is very similar. Here are some examples: -- [`Ruff`](https://docs.astral.sh/ruff/linter/#error-suppression), [`Vulture`](https://github.com/jendrikseipp/vulture?tab=readme-ov-file#flake8-noqa-comments) — `# noqa`, `# noqa: E741, F841`. -- [`Black`](https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#ignoring-sections) and [`Ruff`](https://docs.astral.sh/ruff/formatter/#format-suppression) — `# fmt: on`, `# fmt: off`. -- [`Mypy`](https://discuss.python.org/t/ignore-mypy-specific-type-errors/58535) — `# type: ignore`, `type: ignore[error-code]`. -- [`Coverage`](https://coverage.readthedocs.io/en/7.13.0/excluding.html#default-exclusions) — `# pragma: no cover`, `# pragma: no branch`. -- [`Isort`](https://pycqa.github.io/isort/docs/configuration/action_comments.html) — `# isort: skip`, `# isort: off`. -- [`Bandit`](https://bandit.readthedocs.io/en/latest/config.html#suppressing-individual-lines) — `# nosec`. +- [`Ruff`](https://docs.astral.sh/ruff/linter/#error-suppression), [`Vulture`](https://github.com/jendrikseipp/vulture?tab=readme-ov-file#flake8-noqa-comments) —> `# noqa`, `# noqa: E741, F841`. +- [`Black`](https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#ignoring-sections) and [`Ruff`](https://docs.astral.sh/ruff/formatter/#format-suppression) —> `# fmt: on`, `# fmt: off`. +- [`Mypy`](https://discuss.python.org/t/ignore-mypy-specific-type-errors/58535) —> `# type: ignore`, `type: ignore[error-code]`. +- [`Coverage`](https://coverage.readthedocs.io/en/7.13.0/excluding.html#default-exclusions) —> `# pragma: no cover`, `# pragma: no branch`. +- [`Isort`](https://pycqa.github.io/isort/docs/configuration/action_comments.html) —> `# isort: skip`, `# isort: off`. +- [`Bandit`](https://bandit.readthedocs.io/en/latest/config.html#suppressing-individual-lines) —> `# nosec`. But you know what? *There is no single standard for such comments*. Seriously. From 11dce524f295b6d12a3c84d85fd9d74b802fb429 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: Wed, 10 Dec 2025 01:40:50 +0300 Subject: [PATCH 02/15] Add link to XKCD comic in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e87f38..57ad743 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ The internal implementation of reading such comments is also different. Someone As a result, as a user, you need to remember the rules by which comments are written for each specific tool. And at the same time, you can't be sure that things like double comments (when you want to leave 2 comments for different tools in one line of code) will work in principle. And as the creator of such tools, you are faced with a seemingly simple task — just to read a comment — and find out for yourself that it suddenly turns out to be quite difficult, and there are many possible mistakes. -This is exactly the problem that this library solves. It describes a simple and intuitive standard for action comments, and also offers a ready-made parser that creators of other tools can use. The standard offered by this library is based entirely on a subset of the Python syntax and can be easily reimplemented even if you do not want to use this library directly. +This is exactly the problem that this library solves. It describes a [simple and intuitive standard](https://xkcd.com/927/) for action comments, and also offers a ready-made parser that creators of other tools can use. The standard offered by this library is based entirely on a subset of the Python syntax and can be easily reimplemented even if you do not want to use this library directly. ## The language From 4f44eeae69ed2ebc84fa6921832398379871d1ed 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: Wed, 10 Dec 2025 05:05:33 +0300 Subject: [PATCH 03/15] Remove trailing period after sentence in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 57ad743..5171bac 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ In the Python ecosystem, there are many tools dealing with source code: linters, - [`Isort`](https://pycqa.github.io/isort/docs/configuration/action_comments.html) —> `# isort: skip`, `# isort: off`. - [`Bandit`](https://bandit.readthedocs.io/en/latest/config.html#suppressing-individual-lines) —> `# nosec`. -But you know what? *There is no single standard for such comments*. Seriously. +But you know what? *There is no single standard for such comments*. The internal implementation of reading such comments is also different. Someone uses regular expressions, someone uses even more primitive string processing tools, and someone uses full-fledged parsers, including the Python parser or even written from scratch. From 1cafd3549296ee5c7e97ef63d328d9c037716a7a 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, 12 Dec 2025 02:27:39 +0300 Subject: [PATCH 04/15] Fix description casing for consistency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a929bef..b23a06a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "metacode" version = "0.0.3" authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }] -description = 'The standard language for machine-readable code comments' +description = 'A standard language for machine-readable code comments' readme = "README.md" requires-python = ">=3.8" dependencies = ["libcst>=1.1.0 ; python_version == '3.8'", "libcst>=1.8.6 ; python_version > '3.8'"] From 22fef9b11d205b6d110536d4961e86d0620afe61 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, 12 Dec 2025 04:10:45 +0300 Subject: [PATCH 05/15] Refactor ParsedComment to use new Arguments type alias --- metacode/comment.py | 12 ++++++++++++ metacode/parsing.py | 16 +++------------- metacode/typing.py | 12 ++++++++++++ 3 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 metacode/comment.py create mode 100644 metacode/typing.py diff --git a/metacode/comment.py b/metacode/comment.py new file mode 100644 index 0000000..ae03c14 --- /dev/null +++ b/metacode/comment.py @@ -0,0 +1,12 @@ +from ast import AST +from dataclasses import dataclass +from typing import List, Optional + +from metacode.typing import Arguments + + +@dataclass +class ParsedComment: + key: str + command: str + arguments: Arguments diff --git a/metacode/parsing.py b/metacode/parsing.py index 5c714fc..31c9fb5 100644 --- a/metacode/parsing.py +++ b/metacode/parsing.py @@ -3,24 +3,14 @@ from dataclasses import dataclass from typing import Generator, List, Optional, Union -# TODO: delete this catch block and "type: ignore" if minimum supported version of Python is > 3.9. -try: - from types import EllipsisType # type: ignore[attr-defined, unused-ignore] -except ImportError: # pragma: no cover - EllipsisType = type(...) # type: ignore[misc, unused-ignore] - from libcst import SimpleStatementLine from libcst import parse_module as cst_parse from metacode.errors import UnknownArgumentTypeError +from metacode.typing import Arguments +from metacode.comment import ParsedComment -@dataclass -class ParsedComment: - key: str - command: str - arguments: List[Optional[Union[str, int, float, complex, bool, EllipsisType, AST]]] - def get_right_part(comment: str) -> str: return '#'.join(comment.split('#')[1:]) @@ -57,7 +47,7 @@ def get_candidates(comment: str) -> Generator[ParsedComment, None, None]: assign = parsed_ast.body[0] key = assign.target.id # type: ignore[union-attr] - arguments: List[Optional[Union[str, int, float, complex, bool, EllipsisType, AST]]] = [] + arguments: Arguments = [] if isinstance(assign.annotation, Name): command = assign.annotation.id diff --git a/metacode/typing.py b/metacode/typing.py new file mode 100644 index 0000000..cdcaa7d --- /dev/null +++ b/metacode/typing.py @@ -0,0 +1,12 @@ +from ast import AST +from typing import Union, TypeAlias, List, Optional + +# TODO: delete this catch block and "type: ignore" if minimum supported version of Python is > 3.9. +try: + from types import EllipsisType # type: ignore[attr-defined, unused-ignore] +except ImportError: # pragma: no cover + EllipsisType = type(...) # type: ignore[misc, unused-ignore] + + +Argument: TypeAlias = Union[str, int, float, complex, bool, EllipsisType, AST] +Arguments: TypeAlias = List[Optional[Argument]] From 81bf355355855a3a8c65287f7b5eafdcfbeb0632 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, 12 Dec 2025 04:10:58 +0300 Subject: [PATCH 06/15] Remove unused blank line in parsing.py --- metacode/parsing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/metacode/parsing.py b/metacode/parsing.py index 31c9fb5..b07357b 100644 --- a/metacode/parsing.py +++ b/metacode/parsing.py @@ -11,7 +11,6 @@ from metacode.comment import ParsedComment - def get_right_part(comment: str) -> str: return '#'.join(comment.split('#')[1:]) From b257a77bcabdbe5f7b7e23156019359080aab159 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, 12 Dec 2025 05:30:44 +0300 Subject: [PATCH 07/15] Add build and insert functions for comment creation and insertion --- README.md | 21 +++++ metacode/__init__.py | 5 +- metacode/building.py | 48 +++++++++++ metacode/parsing.py | 1 - tests/test_building.py | 189 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 metacode/building.py create mode 100644 tests/test_building.py diff --git a/README.md b/README.md index 5171bac..600a62a 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,27 @@ print(parse('key: action # other_key: other_action', ['key', 'other_key'])) #> [ParsedComment(key='key', command='action', arguments=[]), ParsedComment(key='other_key', command='other_action', arguments=[])] ``` +Well, now we can read the comments. But what if we want to record? There is another function for this: `insert()`: + +```python +from metacode import insert, ParsedComment +``` + +You send the comment you want to insert there, as well as the current comment (empty if there is no comment, or starting with # if there is), and you get a ready-made new comment text: + +```python +print(insert(ParsedComment(key='key', command='command', arguments=['lol', 'lol-kek']), '')) +# key: command[lol, 'lol-kek'] +print(insert(ParsedComment(key='key', command='command', arguments=['lol', 'lol-kek']), '# some existing text')) +# key: command[lol, 'lol-kek'] # some existing text +``` + +As you can see, our comment is inserted before the existing comment. However, you can do the opposite: + +```python +print(insert(ParsedComment(key='key', command='command', arguments=['lol', 'lol-kek']), '# some existing text', at_end=True)) +# some existing text # key: command[lol, 'lol-kek'] +``` ## What about other languages? diff --git a/metacode/__init__.py b/metacode/__init__.py index 550ffcd..d8f72ff 100644 --- a/metacode/__init__.py +++ b/metacode/__init__.py @@ -1,5 +1,4 @@ -from metacode.errors import ( - UnknownArgumentTypeError as UnknownArgumentTypeError, -) +from metacode.errors import UnknownArgumentTypeError as UnknownArgumentTypeError from metacode.parsing import ParsedComment as ParsedComment from metacode.parsing import parse as parse +from metacode.building import build as build, insert as insert diff --git a/metacode/building.py b/metacode/building.py new file mode 100644 index 0000000..98a60ba --- /dev/null +++ b/metacode/building.py @@ -0,0 +1,48 @@ +from ast import AST + +from metacode.typing import EllipsisType +from metacode import ParsedComment + + +def build(comment: ParsedComment) -> str: + if not comment.key.isidentifier(): + raise ValueError('The key must be valid Python identifier.') + if not comment.command.isidentifier(): + raise ValueError('The command must be valid Python identifier.') + + result = f'# {comment.key}: {comment.command}' + + if comment.arguments: + arguments_representations = [] + + for argument in comment.arguments: + if isinstance(argument, AST): + raise TypeError('AST nodes are read-only and cannot be written to.') + if isinstance(argument, EllipsisType): + arguments_representations.append('...') + elif isinstance(argument, str) and argument.isidentifier(): + arguments_representations.append(argument) + else: + arguments_representations.append(repr(argument)) + + result += f'[{", ".join(arguments_representations)}]' + + return result + + +def insert(comment: ParsedComment, existing_comment: str, at_end: bool = False) -> str: + if not existing_comment: + return build(comment) + + if not existing_comment.lstrip().startswith('#'): + raise ValueError('The existing part of the comment should start with a #.') + + if at_end: + if existing_comment.endswith(' '): + return existing_comment + build(comment) + return f'{existing_comment} {build(comment)}' + + else: + if existing_comment.startswith(' '): + return f'{build(comment)}{existing_comment}' + return f'{build(comment)} {existing_comment}' diff --git a/metacode/parsing.py b/metacode/parsing.py index b07357b..e064602 100644 --- a/metacode/parsing.py +++ b/metacode/parsing.py @@ -1,6 +1,5 @@ from ast import AST, AnnAssign, BinOp, Constant, Index, Name, Sub, Subscript, Tuple from ast import parse as ast_parse -from dataclasses import dataclass from typing import Generator, List, Optional, Union from libcst import SimpleStatementLine diff --git a/tests/test_building.py b/tests/test_building.py new file mode 100644 index 0000000..bd5e106 --- /dev/null +++ b/tests/test_building.py @@ -0,0 +1,189 @@ +from ast import Name + +import pytest +from full_match import match + +from metacode import build, insert, ParsedComment + + +def test_run_build_with_wrong_key_or_action(): + with pytest.raises(ValueError, match=match('The key must be valid Python identifier.')): + build(ParsedComment( + key='123', + command='action', + arguments=[], + )) + + with pytest.raises(ValueError, match=match('The command must be valid Python identifier.')): + build(ParsedComment( + key='key', + command='123', + arguments=[], + )) + + +def test_build_ast(): + with pytest.raises(TypeError, match=match('AST nodes are read-only and cannot be written to.')): + build(ParsedComment( + key='key', + command='command', + arguments=[Name()], + )) + + +def test_create_simple_comment(): + assert build(ParsedComment( + key='key', + command='command', + arguments=[], + )) == '# key: command' + + +def test_create_difficult_comment(): + assert build(ParsedComment( + key='key', + command='command', + arguments=[1], + )) == '# key: command[1]' + + assert build(ParsedComment( + key='key', + command='command', + arguments=[1, 2, 3], + )) == '# key: command[1, 2, 3]' + + assert build(ParsedComment( + key='key', + command='command', + arguments=['build'], + )) == '# key: command[build]' + + assert build(ParsedComment( + key='key', + command='command', + arguments=['build', 'build'], + )) == '# key: command[build, build]' + + assert build(ParsedComment( + key='key', + command='command', + arguments=['lol-kek'], + )) == "# key: command['lol-kek']" + + assert build(ParsedComment( + key='key', + command='command', + arguments=['lol-kek', 'lol-kek-chedurek'], + )) == "# key: command['lol-kek', 'lol-kek-chedurek']" + + assert build(ParsedComment( + key='key', + command='command', + arguments=[...], + )) == "# key: command[...]" + + assert build(ParsedComment( + key='key', + command='command', + arguments=[..., ...], + )) == "# key: command[..., ...]" + + assert build(ParsedComment( + key='key', + command='command', + arguments=[1.5], + )) == "# key: command[1.5]" + + assert build(ParsedComment( + key='key', + command='command', + arguments=[1.5, 3.0], + )) == "# key: command[1.5, 3.0]" + + assert build(ParsedComment( + key='key', + command='command', + arguments=[5j], + )) == "# key: command[5j]" + + assert build(ParsedComment( + key='key', + command='command', + arguments=[None], + )) == "# key: command[None]" + + assert build(ParsedComment( + key='key', + command='command', + arguments=[True], + )) == "# key: command[True]" + + assert build(ParsedComment( + key='key', + command='command', + arguments=[False], + )) == "# key: command[False]" + + assert build(ParsedComment( + key='key', + command='command', + arguments=[1, 2, 3, 1.5, 3.0, 5j, 1000j, 'build', 'build2', 'lol-kek', 'lol-kek-chedurek', None, True, False, ...], + )) == "# key: command[1, 2, 3, 1.5, 3.0, 5j, 1000j, build, build2, 'lol-kek', 'lol-kek-chedurek', None, True, False, ...]" + + +def test_insert_to_strange_comment(): + with pytest.raises(ValueError, match=match('The existing part of the comment should start with a #.')): + insert(ParsedComment(key='key', command='command', arguments=[]), 'kek', at_end=True) + + with pytest.raises(ValueError, match=match('The existing part of the comment should start with a #.')): + insert(ParsedComment(key=' key', command='command', arguments=[]), 'kek', at_end=True) + + with pytest.raises(ValueError, match=match('The existing part of the comment should start with a #.')): + insert(ParsedComment(key=' key', command='command', arguments=[]), 'kek') + + with pytest.raises(ValueError, match=match('The existing part of the comment should start with a #.')): + insert(ParsedComment(key='key', command='command', arguments=[]), 'kek') + + +def test_insert_at_begin_to_empty(): + comment = ParsedComment( + key='key', + command='command', + arguments=['build'], + ) + + assert insert(comment, '') == build(comment) + + +def test_insert_at_end_to_empty(): + comment = ParsedComment( + key='key', + command='command', + arguments=['build'], + ) + + assert insert(comment, '', at_end=True) == build(comment) + + +def test_insert_at_begin_to_not_empty(): + comment = ParsedComment( + key='key', + command='command', + arguments=['build'], + ) + + assert insert(comment, '# kek') == build(comment) + ' # kek' + assert insert(comment, ' # kek') == build(comment) + ' # kek' + assert insert(comment, build(comment)) == build(comment) + ' ' + build(comment) + + +def test_insert_at_end_to_not_empty(): + comment = ParsedComment( + key='key', + command='command', + arguments=['build'], + ) + + assert insert(comment, '# kek', at_end=True) == '# kek ' + build(comment) + assert insert(comment, '# kek ', at_end=True) == '# kek ' + build(comment) + assert insert(comment, build(comment), at_end=True) == build(comment) + ' ' + build(comment) From 17650e2c0ce670f2d88101418b601d0fd0b00f26 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, 12 Dec 2025 05:32:55 +0300 Subject: [PATCH 08/15] Add warning about AST node read-only nature --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 600a62a..4e908be 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,9 @@ print(insert(ParsedComment(key='key', command='command', arguments=['lol', 'lol- # some existing text # key: command[lol, 'lol-kek'] ``` +> ⚠️ Be careful: AST nodes can be read, but cannot be written. + + ## What about other languages? If you are writing your Python-related tool not in Python, as is currently fashionable, but in some other language, such as Rust, you may want to adhere to the `metacode` standard for machine-readable comments, however, you cannot directly use the ready-made parser described [above](#usage). What to do? From d8654868ed311927ce4bfe260822f2d3f79b896e 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, 12 Dec 2025 05:33:41 +0300 Subject: [PATCH 09/15] Bump version to 0.0.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b23a06a..9ffda60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "metacode" -version = "0.0.3" +version = "0.0.4" authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }] description = 'A standard language for machine-readable code comments' readme = "README.md" From 90f8d4bacdeae15fd710b77a243078e4086c08ee 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, 12 Dec 2025 05:34:41 +0300 Subject: [PATCH 10/15] Refactor building module imports and fix insert logic --- metacode/__init__.py | 3 ++- metacode/building.py | 9 ++++----- metacode/comment.py | 2 -- metacode/parsing.py | 2 +- metacode/typing.py | 2 +- tests/test_building.py | 2 +- 6 files changed, 9 insertions(+), 11 deletions(-) diff --git a/metacode/__init__.py b/metacode/__init__.py index d8f72ff..70c983b 100644 --- a/metacode/__init__.py +++ b/metacode/__init__.py @@ -1,4 +1,5 @@ +from metacode.building import build as build +from metacode.building import insert as insert from metacode.errors import UnknownArgumentTypeError as UnknownArgumentTypeError from metacode.parsing import ParsedComment as ParsedComment from metacode.parsing import parse as parse -from metacode.building import build as build, insert as insert diff --git a/metacode/building.py b/metacode/building.py index 98a60ba..0130553 100644 --- a/metacode/building.py +++ b/metacode/building.py @@ -1,7 +1,7 @@ from ast import AST -from metacode.typing import EllipsisType from metacode import ParsedComment +from metacode.typing import EllipsisType def build(comment: ParsedComment) -> str: @@ -42,7 +42,6 @@ def insert(comment: ParsedComment, existing_comment: str, at_end: bool = False) return existing_comment + build(comment) return f'{existing_comment} {build(comment)}' - else: - if existing_comment.startswith(' '): - return f'{build(comment)}{existing_comment}' - return f'{build(comment)} {existing_comment}' + if existing_comment.startswith(' '): + return f'{build(comment)}{existing_comment}' + return f'{build(comment)} {existing_comment}' diff --git a/metacode/comment.py b/metacode/comment.py index ae03c14..a0a8f92 100644 --- a/metacode/comment.py +++ b/metacode/comment.py @@ -1,6 +1,4 @@ -from ast import AST from dataclasses import dataclass -from typing import List, Optional from metacode.typing import Arguments diff --git a/metacode/parsing.py b/metacode/parsing.py index e064602..fcbc479 100644 --- a/metacode/parsing.py +++ b/metacode/parsing.py @@ -5,9 +5,9 @@ from libcst import SimpleStatementLine from libcst import parse_module as cst_parse +from metacode.comment import ParsedComment from metacode.errors import UnknownArgumentTypeError from metacode.typing import Arguments -from metacode.comment import ParsedComment def get_right_part(comment: str) -> str: diff --git a/metacode/typing.py b/metacode/typing.py index cdcaa7d..d0fe1d4 100644 --- a/metacode/typing.py +++ b/metacode/typing.py @@ -1,5 +1,5 @@ from ast import AST -from typing import Union, TypeAlias, List, Optional +from typing import List, Optional, TypeAlias, Union # TODO: delete this catch block and "type: ignore" if minimum supported version of Python is > 3.9. try: diff --git a/tests/test_building.py b/tests/test_building.py index bd5e106..fb7f06b 100644 --- a/tests/test_building.py +++ b/tests/test_building.py @@ -3,7 +3,7 @@ import pytest from full_match import match -from metacode import build, insert, ParsedComment +from metacode import ParsedComment, build, insert def test_run_build_with_wrong_key_or_action(): From 73f3b56a68301b2819190f4ac68bf7c0470d493c 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, 12 Dec 2025 05:40:49 +0300 Subject: [PATCH 11/15] Update import paths and add typing_extensions for Python <= 3.9 support --- metacode/building.py | 2 +- metacode/typing.py | 9 +++++++-- pyproject.toml | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/metacode/building.py b/metacode/building.py index 0130553..335cb42 100644 --- a/metacode/building.py +++ b/metacode/building.py @@ -1,6 +1,6 @@ from ast import AST -from metacode import ParsedComment +from metacode.comment import ParsedComment from metacode.typing import EllipsisType diff --git a/metacode/typing.py b/metacode/typing.py index d0fe1d4..4e508d4 100644 --- a/metacode/typing.py +++ b/metacode/typing.py @@ -1,7 +1,12 @@ from ast import AST -from typing import List, Optional, TypeAlias, Union +from typing import List, Optional, Union + +# TODO: delete this catch blocks and "type: ignore" if minimum supported version of Python is > 3.9. +try: + from typing import TypeAlias +except ImportError: # pragma: no cover + from typing_extensions import TypeAlias -# TODO: delete this catch block and "type: ignore" if minimum supported version of Python is > 3.9. try: from types import EllipsisType # type: ignore[attr-defined, unused-ignore] except ImportError: # pragma: no cover diff --git a/pyproject.toml b/pyproject.toml index 9ffda60..7a66f74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }] description = 'A standard language for machine-readable code comments' readme = "README.md" requires-python = ">=3.8" -dependencies = ["libcst>=1.1.0 ; python_version == '3.8'", "libcst>=1.8.6 ; python_version > '3.8'"] +dependencies = ["libcst>=1.1.0 ; python_version == '3.8'", "libcst>=1.8.6 ; python_version > '3.8'", "typing_extensions ; python_version <= '3.9'"] classifiers = [ "Operating System :: OS Independent", 'Operating System :: MacOS :: MacOS X', From 6adb16b633c59e98f6200c204eac0dada73601dd 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, 12 Dec 2025 05:46:24 +0300 Subject: [PATCH 12/15] Silence type checker for unused imports --- metacode/__init__.py | 2 +- metacode/building.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/metacode/__init__.py b/metacode/__init__.py index 70c983b..f75aa10 100644 --- a/metacode/__init__.py +++ b/metacode/__init__.py @@ -1,5 +1,5 @@ from metacode.building import build as build from metacode.building import insert as insert from metacode.errors import UnknownArgumentTypeError as UnknownArgumentTypeError -from metacode.parsing import ParsedComment as ParsedComment +from metacode.comment import ParsedComment as ParsedComment from metacode.parsing import parse as parse diff --git a/metacode/building.py b/metacode/building.py index 335cb42..9637caf 100644 --- a/metacode/building.py +++ b/metacode/building.py @@ -1,7 +1,7 @@ from ast import AST from metacode.comment import ParsedComment -from metacode.typing import EllipsisType +from metacode.typing import EllipsisType # type: ignore[attr-defined] def build(comment: ParsedComment) -> str: From aa63737d23c677f5716fcbd7fd4cefca65454167 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, 12 Dec 2025 05:48:50 +0300 Subject: [PATCH 13/15] Fix import order in __init__.py --- metacode/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metacode/__init__.py b/metacode/__init__.py index f75aa10..277e362 100644 --- a/metacode/__init__.py +++ b/metacode/__init__.py @@ -1,5 +1,5 @@ from metacode.building import build as build from metacode.building import insert as insert -from metacode.errors import UnknownArgumentTypeError as UnknownArgumentTypeError from metacode.comment import ParsedComment as ParsedComment +from metacode.errors import UnknownArgumentTypeError as UnknownArgumentTypeError from metacode.parsing import parse as parse From d55f28cf3daaace5fc081071232da80df7e57440 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, 12 Dec 2025 05:57:48 +0300 Subject: [PATCH 14/15] Add "type: ignore" to TypeAlias import for Python <= 3.9 --- metacode/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metacode/typing.py b/metacode/typing.py index 4e508d4..8792b17 100644 --- a/metacode/typing.py +++ b/metacode/typing.py @@ -3,7 +3,7 @@ # TODO: delete this catch blocks and "type: ignore" if minimum supported version of Python is > 3.9. try: - from typing import TypeAlias + from typing import TypeAlias # type: ignore[attr-defined] except ImportError: # pragma: no cover from typing_extensions import TypeAlias From f72dabe43b7f82e7a4575b5dd2c6542bb8cc3f82 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, 12 Dec 2025 06:00:40 +0300 Subject: [PATCH 15/15] Add unused-ignore to TypeAlias import ignore --- metacode/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metacode/typing.py b/metacode/typing.py index 8792b17..fc0baf9 100644 --- a/metacode/typing.py +++ b/metacode/typing.py @@ -3,7 +3,7 @@ # TODO: delete this catch blocks and "type: ignore" if minimum supported version of Python is > 3.9. try: - from typing import TypeAlias # type: ignore[attr-defined] + from typing import TypeAlias # type: ignore[attr-defined, unused-ignore] except ImportError: # pragma: no cover from typing_extensions import TypeAlias