Release develop into main - #66
Merged
Merged
Conversation
- Remove deprecated ast.Str branches in the analyzer; ast.Constant already covers string literals on Python 3.8+ and ast.Str emits a DeprecationWarning on 3.12. - Add tests for PEP 695 syntax (type aliases, generic functions/classes), skipped on Python < 3.12. - Add the Python 3.12 classifier, tox env and black target versions. - Update the changelog. Closes #28 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
- Add tests for PEP 696 type parameter defaults, skipped on Python < 3.13. - Add the Python 3.13 classifier, tox env and black target version. - Update the changelog. Closes #29 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
- Run the test matrix on Python 3.14. - Add tests for PEP 750 template strings, PEP 758 unparenthesized except expressions and PEP 649 deferred annotations, skipped on Python < 3.14. - Add the Python 3.14 classifier and tox env. - Update the changelog. Closes #30 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
TypeVar, ParamSpec and TypeVarTuple definitions (called directly or through a module attribute such as typing.TypeVar) are module-local helpers, so they are no longer treated as public names. An '# unexport: public' comment still adds them. Also document the behaviour and fix the unbalanced code fences in the useful-features tutorial. Closes #9 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
When a module already has an __all__ assignment, replace only the source span of its value instead of deleting and rewriting the whole statement. Comments after __all__, other statements on the same line and the target/spacing before the value are now kept. AST column offsets are UTF-8 byte offsets, so the slicing is done on the encoded line to stay correct with non-ASCII text before or after the value. Closes #12 Closes #7 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
- Keep the type of an existing __all__ (list, tuple or set) instead of always writing a list; a single-item tuple gets its trailing comma. - Write one name per line, black style with a trailing comma, when the existing __all__ was already multi-line or the one-line form would be longer than 88 characters. The closing bracket follows the indentation of the __all__ line. - Fix a crash on modules whose __all__ is a set: the rule accepted ast.Set but the analyzer asserted list or tuple. Closes #22 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
- Drop Python 3.8 and 3.9, which are end-of-life and were no longer tested in CI: python_requires, classifiers, tox envs and black targets. - Run pyupgrade with --py310-plus; it moves Callable/Iterable/Iterator/ Sequence imports from typing to collections.abc. - Update the install and contributing docs to the new minimum. - Change the development status classifier from Planning to Beta. - Remove a stray blank line in the Unreleased changelog section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
Support Python 3.12
Support Python 3.13
Support Python 3.14
Require Python 3.10+ and mark the project as Beta
…nto claude/issue-9-exclude-typevars # Conflicts: # docs/changelog.md
…vars Don't add TypeVars to __all__
…nto claude/issue-12-token-level-refactor # Conflicts: # docs/changelog.md
…-refactor Refactor __all__ at the token level (keeps comments)
…nto claude/issue-22-beautiful-all
Format the refactored __all__ beautifully
With TypeVars no longer treated as public (#9), unexport's own check flagged typing.Function, which the other modules import. Mark it public explicitly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
Analyzer.expected_all concatenated the class, function and variable name lists, so `class Point: ...` followed by `Point = Point` produced ["Point", "Point"]. The lists are now merged as a set. Closes #43 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
List a name once when it is both a class/function and a variable
unexport derived the expected __all__ only from public classes, functions and variables, so everything else that a user had listed was dropped: re-exported imports (the usual content of __init__.py), dunders such as __version__, lowercase variables, and names provided by a star import. --refactor removed them from the package API. The analyzer now also collects the names bound at module level (imports, classes, functions, variables; not function bodies or comprehension targets) and whether the module has a star import. A name already in __all__ stays when the module binds it, or when a star import may provide it, unless it is marked `# unexport: not-public`. Imports are still not added on their own, and listed names that no longer exist are still removed. Closes #39 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
Keep names already listed in __all__ that the module still binds
refactor_source returned early when the expected __all__ was empty, so an existing __all__ that only listed names that no longer exist was never fixed, while main still printed "Refactoring" and the check kept failing. An existing __all__ is now rewritten to its empty form ([] or (), on one line), and a module without __all__ and without public names is still left alone. "Refactoring" is only printed when the file actually changed. Closes #45 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
Rewrite a stale __all__ to empty when nothing is public
Without imports, the new __all__ was inserted at line 1, above the module docstring, which then stopped being the docstring (__doc__ became None). A shebang or encoding line was pushed down too, where it no longer takes effect. The insertion point is now after the last top-level import as before; without imports, after the module docstring; otherwise after a leading shebang / PEP 263 encoding line. Closes #42 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
Insert a new __all__ after the module docstring
- main() built the syntax-error message with color.paint() but never printed it, so such files were skipped silently with exit code 0 (and the message lacked a space before "at"). - utils.read() did not catch UnicodeDecodeError, so one file with invalid bytes crashed the whole run; OSError and bad encoding declarations returned an empty source and were silently treated as clean. read() now raises utils.READ_ERRORS; Session.get_source() yields the error with the path. main() prints "<error> at <path>" for read and syntax errors, sets the exit code to 1 and continues with the other files. Closes #44 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
Report syntax errors and unreadable files
Every capitalized module-level store was exported, including names that are gone when the module is imported, so `from module import *` failed with AttributeError: - names bound only in the body of `if TYPE_CHECKING:` (or `typing.TYPE_CHECKING`) or `if __name__ == "__main__":`; the else branch does run and still counts; - comprehension targets, which are local to the comprehension (a walrus inside a comprehension binds at module level and still counts); - names removed with `del` after their last binding. `# unexport: public` still forces a name. Names already listed in __all__ are no longer kept (#39) when they only exist in those places. Closes #40 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
Don't export names that don't exist at import time
Read every __all__ form and don't rewrite ones that can't be safely updated
When the last top-level statement was an import, the insert point was the end of the file: without a trailing newline this raised IndexError, and with one an extra blank line was left at the end (ast._splitlines_no_ff yields a trailing empty string). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
{} is an empty dict, which changes the type of __all__.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
An annotation without a value binds nothing at runtime, so listing it in __all__ breaks `from module import *`. It is neither added nor kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
`T, U = TypeVar("T"), TypeVar("U")` was not recognized because only a
Call assigned directly to the name was checked. The rule now follows the
target's position through nested tuple/list unpacking to its value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
The skip flag was only set on class, function and name nodes, so a listed re-exported import could not be removed from __all__ with the comment. It is now set on each imported name, from a comment on the statement's first line or on the name's own line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
Before Python 3.12, ast.parse raises ValueError for source with null bytes, which escaped the SyntaxError handling and stopped the whole run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
Fix inserting __all__ after an import that ends the file
…orted Don't export names that are only annotated (X: int)
Don't export TypeVars bound by tuple unpacking
Report files with null bytes as errors instead of crashing
…nto fix/empty-set-all # Conflicts: # tests/test_refactor.py
…nto fix/not-public-imports # Conflicts: # src/unexport/analyzer.py
Write set() instead of {} when a set __all__ becomes empty
Honor # unexport: not-public on imports
…d on the same line - `if False:` / `if 0:` bodies and the else of `if True:` / `if not TYPE_CHECKING:` don't run at import time either. - A walrus inside a lambda binds a local of the lambda. - `X = 1; del X` compared line numbers only, so the del did not count; bindings and deletions are now ordered by (line, column). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
…g it `from io import __all__` (as in stdlib _pyio) was not recognized as defining __all__, so a new `__all__ = [...]` was inserted after the imports and `from m import *` lost the imported names. An __all__ bound by an import or inside an unpacking/for target is now dynamic: it is not checked or rewritten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
These were removed from __all__, so `from m import *` silently lost them: - submodules listed in a package's __init__.py, which the star import loads - names served by a module __getattr__ (PEP 562) - names added through globals() / vars() or @enum.global_enum - match-statement captures, which were not seen as module bindings Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
tokenize.open translates newlines while reading and write_text wrote \n, so refactoring a CRLF file rewrote every line to LF. read() now also returns the newline it saw, and the file is written back with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
Don't export names from never-run branches, lambda walruses or deleted on the same line
Treat an imported or unpacked __all__ as dynamic instead of overriding it
Keep the file's newline style when refactoring
…nto fix/keep-runtime-provided-names # Conflicts: # src/unexport/analyzer.py # src/unexport/session.py
…ames Keep listed names that exist at runtime without a visible binding
- Rewriting a multi-line __all__ rebuilt the literal and deleted comments inside it. Such an __all__ is now reported but not rewritten. - The message for an __all__ that can't be rewritten said it was built from several statements, also when it was a non-literal like ["A"] + ["B"]; it now names all the reasons. - `# unexport: not-public` / `public` were matched against whole physical lines, so the text inside a string literal marked the name. Only comment tokens count now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
`if sys.platform == "win32": class WinOnly: ...` added WinOnly to __all__, so `from module import *` failed on every other platform. A name is only added when every branch of such an if binds it (following elif chains). Listed names are still kept and `# unexport: public` still adds one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
Don't drop comments inside __all__, and only read markers from comments
…nto fix/conditional-names # Conflicts: # docs/changelog.md
Don't add names that are bound in only one branch of an undecidable if
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings everything reviewed on
developintomain: 60 commits, from PRs #31–#36, #38 and #46–#64.Python support
ast.Stris no longer used (3.14).__all__correctness__all__are kept (Re-exported imports are removed from __all__ #39).# unexport: not-publicworks on imports (Honor # unexport: not-public on imports #57).__all__is treated as dynamic (Treat an imported or unpacked __all__ as dynamic instead of overriding it #60).del, includingX = 1; del Xon one lineif TYPE_CHECKING:, the main guard orif False:X: int)if(Don't add names that are bound in only one branch of an undecidable if #64)__getattr__,globals()/@enum.global_enumand match captures.__all__handling__all__is emptied (A stale __all__ is never fixed when nothing is public anymore #45); an empty set becomesset()(Write set() instead of {} when a set __all__ becomes empty #54).__all__form is read (+=, annotated,append/extend, insideif/try). One built from several statements, or holding comments, is reported but not rewritten (__all__ built with +=, an annotation, extend(tuple) or concatenation is misread and rewritten incorrectly #41, Don't drop comments inside __all__, and only read markers from comments #63).Refactoring
__all__is inserted after the docstring, shebang or coding line (New __all__ is inserted above the module docstring #42), and correctly when the file ends with an import (Fix inserting __all__ after an import that ends the file #53).CLI
# unexport:markers are only read from comments (Don't drop comments inside __all__, and only read markers from comments #63).Checks
CI passed on every merged PR. The combined branch passes on 3.10 / 3.12 / 3.14, and pre-commit, mypy and the self-check pass.
Closes #37
🤖 Generated with Claude Code
https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
Generated by Claude Code