Skip to content

Release develop into main - #66

Merged
hakancelikdev merged 60 commits into
mainfrom
develop
Oct 2, 2026
Merged

hakancelikdev merged 60 commits into
mainfrom
develop

Conversation

@hakancelikdev

Copy link
Copy Markdown
Owner

Brings everything reviewed on develop into main: 60 commits, from PRs #31–#36, #38 and #46–#64.

Python support

__all__ correctness

Refactoring

CLI

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

claude and others added 30 commits September 23, 2026 21:56
- 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
Require Python 3.10+ and mark the project as Beta
…nto claude/issue-9-exclude-typevars

# Conflicts:
#	docs/changelog.md
…nto claude/issue-12-token-level-refactor

# Conflicts:
#	docs/changelog.md
…-refactor

Refactor __all__ at the token level (keeps comments)
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
hakancelikdev and others added 29 commits September 25, 2026 21:35
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
@hakancelikdev
hakancelikdev merged commit a312577 into main Oct 2, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Merge plan and remaining work for open PRs #31–#38

2 participants