Skip to content

Commit

Permalink
Read version from version files via ast
Browse files Browse the repository at this point in the history
In cases where packages import `__version__` from version modules (like
those generated by setuptools_scm or attribution), this allows flit-core
to use the existing ast-based logic to read the version directly from
those files when possible. It checks a few well-known version filenames,
including the package-level `__init__.py` as usual, with `__init__.py`
taking precedence over external version files if an assigned
`__version__` value exists in multiple places.

This makes flit more resilient to reading version from these packages,
even if their `__init__.py` has transitive imports that aren't available
in the environment at build time (eg, conda-forge builds). It also
reduces the number of times when flit has to fall back to actually
importing the `__init__.py` file rather than reading with ast.
  • Loading branch information
amyreese committed Apr 8, 2023
1 parent 532bdaa commit 88b0224
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 20 deletions.
51 changes: 31 additions & 20 deletions flit_core/flit_core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ def file(self):
else:
return self.path

@property
def version_files(self):
if self.is_package:
paths = [self.path / '__init__.py']
for filename in ('version.py', '_version.py', '__version__.py'):
if (self.path / filename).is_file():
paths.insert(0, self.path / filename)
return paths
else:
return [self.path]

def iter_files(self):
"""Iterate over the files contained in this module.
Expand Down Expand Up @@ -127,26 +138,26 @@ def get_docstring_and_version_via_ast(target):
Return a tuple like (docstring, version) for the given module,
extracted by parsing its AST.
"""
# read as bytes to enable custom encodings
with target.file.open('rb') as f:
node = ast.parse(f.read())
for child in node.body:
# Only use the version from the given module if it's a simple
# string assignment to __version__
is_version_str = (
isinstance(child, ast.Assign)
and any(
isinstance(target, ast.Name)
and target.id == "__version__"
for target in child.targets
)
and isinstance(child.value, ast.Str)
)
if is_version_str:
version = child.value.s
break
else:
version = None
version = None
for target_path in target.version_files:
# read as bytes to enable custom encodings
with target_path.open('rb') as f:
node = ast.parse(f.read())
for child in node.body:
# Only use the version from the given module if it's a simple
# string assignment to __version__
is_version_str = (
isinstance(child, ast.Assign)
and any(
isinstance(target, ast.Name)
and target.id == "__version__"
for target in child.targets
)
and isinstance(child.value, ast.Str)
)
if is_version_str:
version = child.value.s
break
return ast.get_docstring(node), version


Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""This module has a __version__ that requires a relative import"""

from ._version import __version__

import a_package_that_doesnt_exist
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
"""Imposter docstring that shouldn't be used"""

__version__ = '0.5.8'

0 comments on commit 88b0224

Please sign in to comment.