Skip to content

Commit 1c351e6

Browse files
committed
Format the setuptools_scm version file after it is written
The setuptools_scm handler makes setuptools_scm write the configured `version_file`, and that file is then left in setuptools_scm's own layout, so the format check flags it after every version query. - Add FormatSetuptoolsScmVersionFileHandler, registered after the scm handler. It runs `format_file` on the written version file and returns the version unchanged. Which formatter runs stays `format_file`'s dispatch decision; a file no formatter covers is reported as coverage, not absorbed, while a formatter failure fails the run. - Read the content from disk rather than through the file editor: setuptools_scm writes past the editor, so an open buffer would hold the previous version and saving it would overwrite the fresh write. - Pass `force_write_version_files=True` so the file is rewritten on every query and there is always a fresh file to format. - Move config loading and version-file path resolution into `_scm_config` so both handlers share them. - Register the handler in the root and extension-runner pyproject, add the `fine_format` dependency, document it in the actions reference, and add tests for both handlers.
1 parent 3edc8b9 commit 1c351e6

11 files changed

Lines changed: 755 additions & 21 deletions

‎docs/reference/actions.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ Get the current version of a source artifact.
332332
- **Source:** `fine_src_artifacts.GetSrcArtifactVersionAction`
333333

334334
Default handler in this repo: `fine_python_setuptools_scm.GetSrcArtifactVersionSetuptoolsScmHandler`
335+
In this repo it is followed by `fine_python_setuptools_scm.FormatSetuptoolsScmVersionFileHandler`, which formats the written `[tool.setuptools_scm] version_file` with the project's `format_file` formatter; disable that handler to leave the file as setuptools_scm writes it.
335336

336337
---
337338

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
from .format_setuptools_scm_version_file_handler import (
2+
FormatSetuptoolsScmVersionFileHandler,
3+
FormatSetuptoolsScmVersionFileHandlerConfig,
4+
)
15
from .get_src_artifact_version_setuptools_scm_handler import (
26
GetSrcArtifactVersionSetuptoolsScmHandler,
37
GetSrcArtifactVersionSetuptoolsScmHandlerConfig,
48
)
59

610
__all__ = [
11+
"FormatSetuptoolsScmVersionFileHandler",
12+
"FormatSetuptoolsScmVersionFileHandlerConfig",
713
"GetSrcArtifactVersionSetuptoolsScmHandler",
814
"GetSrcArtifactVersionSetuptoolsScmHandlerConfig",
915
]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pathlib
2+
3+
from finecode_extension_api.interfaces import ilogger, iprojectinfoprovider
4+
from finecode_extension_api.resource_uri import ResourceUri, resource_uri_to_path
5+
from setuptools_scm import Configuration
6+
7+
8+
def resolve_def_path(
9+
payload_def_path: ResourceUri | None,
10+
project_info_provider: iprojectinfoprovider.IProjectInfoProvider,
11+
) -> pathlib.Path:
12+
if payload_def_path is not None:
13+
return resource_uri_to_path(payload_def_path)
14+
return project_info_provider.get_current_project_def_path()
15+
16+
17+
def load_configuration(
18+
def_path: pathlib.Path, logger: ilogger.ILogger
19+
) -> Configuration:
20+
pyproject = def_path.as_posix()
21+
try:
22+
# could be optimized by providing config from project_info_provider instead
23+
# of reading file each time
24+
config = Configuration.from_file(pyproject, root=None)
25+
except (LookupError, FileNotFoundError) as ex:
26+
# no pyproject.toml OR no [tool.setuptools_scm]
27+
logger.warning(
28+
f"Warning: could not use {pyproject},"
29+
" using default configuration.\n"
30+
f" Reason: {ex}."
31+
)
32+
config = Configuration(root=def_path.parent.as_posix())
33+
return config
34+
35+
36+
def version_file_path(
37+
config: Configuration, logger: ilogger.ILogger
38+
) -> pathlib.Path | None:
39+
if getattr(config, "write_to", None):
40+
logger.debug(
41+
"get_src_artifact_version_setuptools_scm_format: ignoring legacy"
42+
" [tool.setuptools_scm] write_to; only version_file is formatted"
43+
)
44+
if not config.version_file or config.relative_to is None:
45+
return None
46+
return pathlib.Path(config.relative_to).parent / config.version_file
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import dataclasses
2+
3+
from fine_format import format_file_action
4+
from fine_src_artifacts import get_src_artifact_version_action
5+
from finecode_extension_api import code_action
6+
from finecode_extension_api.code_action import CoverageStatus, ItemCoverage
7+
from finecode_extension_api.interfaces import (
8+
ifileeditor,
9+
ifilemanager,
10+
ilogger,
11+
iprojectactionrunner,
12+
iprojectinfoprovider,
13+
)
14+
from finecode_extension_api.resource_uri import path_to_resource_uri
15+
16+
from ._scm_config import load_configuration, resolve_def_path, version_file_path
17+
18+
19+
@dataclasses.dataclass
20+
class FormatSetuptoolsScmVersionFileHandlerConfig(code_action.ActionHandlerConfig): ...
21+
22+
23+
class FormatSetuptoolsScmVersionFileHandler(
24+
code_action.ActionHandler[
25+
get_src_artifact_version_action.GetSrcArtifactVersionAction,
26+
FormatSetuptoolsScmVersionFileHandlerConfig,
27+
]
28+
):
29+
"""Format the setuptools_scm version file with the project's formatter.
30+
31+
The file formatted is the ``[tool.setuptools_scm] version_file`` that the
32+
scm handler caused setuptools_scm to write. Which formatter runs is decided
33+
by ``format_file``'s dispatch on the target file, never here.
34+
35+
No formatter covering the target file is not an error: the miss stays
36+
visible as coverage and is never absorbed. A formatter that fails does fail
37+
the run; disable this handler to leave the file as setuptools_scm writes
38+
it.
39+
40+
The content is read from disk, not through the file editor, because
41+
setuptools_scm writes past the editor: a buffer opened in the IDE would
42+
still hold the previous version text, and formatting that stale content
43+
would save it over the fresh write.
44+
45+
This handler must run after a handler that determines the version, and it
46+
returns that version unchanged.
47+
"""
48+
49+
FILE_OPERATION_AUTHOR = ifileeditor.FileOperationAuthor(
50+
id="FormatSetuptoolsScmVersionFileHandler"
51+
)
52+
53+
def __init__(
54+
self,
55+
action_runner: iprojectactionrunner.IProjectActionRunner,
56+
file_editor: ifileeditor.IFileEditor,
57+
file_manager: ifilemanager.IFileManager,
58+
project_info_provider: iprojectinfoprovider.IProjectInfoProvider,
59+
logger: ilogger.ILogger,
60+
) -> None:
61+
self.action_runner = action_runner
62+
self.file_editor = file_editor
63+
self.file_manager = file_manager
64+
self.project_info_provider = project_info_provider
65+
self.logger = logger
66+
67+
async def run(
68+
self,
69+
payload: get_src_artifact_version_action.GetSrcArtifactVersionRunPayload,
70+
run_context: get_src_artifact_version_action.GetSrcArtifactVersionRunContext,
71+
) -> get_src_artifact_version_action.GetSrcArtifactVersionRunResult:
72+
current_result = run_context.current_result
73+
if not isinstance(
74+
current_result,
75+
get_src_artifact_version_action.GetSrcArtifactVersionRunResult,
76+
):
77+
raise code_action.ActionFailedException(
78+
"get_src_artifact_version_setuptools_scm_format: no version from"
79+
" a previous handler; register it after a handler that determines"
80+
" the version"
81+
)
82+
version = current_result.version
83+
84+
def_path = resolve_def_path(
85+
payload.src_artifact_def_path, self.project_info_provider
86+
)
87+
config = load_configuration(def_path, self.logger)
88+
target = version_file_path(config, self.logger)
89+
if target is None or not target.is_file():
90+
return get_src_artifact_version_action.GetSrcArtifactVersionRunResult(
91+
version=version
92+
)
93+
94+
content = await self.file_manager.get_content(target)
95+
coverage: list[ItemCoverage] = []
96+
try:
97+
result = await self.action_runner.run_action(
98+
action_type=iprojectactionrunner.ActionRef.from_type(
99+
format_file_action.FormatFileAction
100+
),
101+
payload=format_file_action.FormatFileRunPayload(
102+
file_path=path_to_resource_uri(target), save=False
103+
),
104+
meta=run_context.meta,
105+
caller_kwargs=format_file_action.FormatFileCallerRunContextKwargs(
106+
file_editor_session=None, # non-serializable; must stay None
107+
file_info=format_file_action.FileInfo(
108+
file_content=content,
109+
file_version="",
110+
),
111+
),
112+
)
113+
except iprojectactionrunner.ActionNotFound:
114+
# No dispatcher ran to record the miss, so record it here: the
115+
# version file has no formatter, the same answer as "no subactions
116+
# registered".
117+
coverage = [
118+
ItemCoverage(
119+
status=CoverageStatus.NO_SUBACTIONS,
120+
item=path_to_resource_uri(target),
121+
detail="format_file",
122+
)
123+
]
124+
self.logger.warning(
125+
"get_src_artifact_version_setuptools_scm_format: no format_file"
126+
" action registered; leaving the version file unformatted"
127+
)
128+
except iprojectactionrunner.ActionRunFailed as exc:
129+
raise code_action.ActionFailedException(
130+
f"Formatting the version file {target} failed (disable handler"
131+
" 'get_src_artifact_version_setuptools_scm_format' to keep it"
132+
f" unformatted):\n - {exc.message}"
133+
) from exc
134+
else:
135+
if result.unhandled:
136+
# Not absorbed: the miss reaches this run's result through the
137+
# coverage sink, which is how the caller learns the file is
138+
# unformatted.
139+
self.logger.warning(
140+
"get_src_artifact_version_setuptools_scm_format: no formatter"
141+
" covers the version file; leaving it unformatted"
142+
)
143+
elif result.changed:
144+
async with self.file_editor.session(
145+
author=self.FILE_OPERATION_AUTHOR
146+
) as session:
147+
await session.save_file(
148+
file_path=target,
149+
file_content=result.code,
150+
)
151+
152+
return get_src_artifact_version_action.GetSrcArtifactVersionRunResult(
153+
version=version, coverage=coverage
154+
)

‎extensions/fine_python_setuptools_scm/fine_python_setuptools_scm/get_src_artifact_version_setuptools_scm_handler.py‎

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
from fine_src_artifacts import get_src_artifact_version_action
44
from finecode_extension_api import code_action
55
from finecode_extension_api.interfaces import ilogger, iprojectinfoprovider
6-
from setuptools_scm import Configuration
76
from setuptools_scm._get_version import _get_version
87

8+
from ._scm_config import load_configuration, resolve_def_path
9+
910

1011
@dataclasses.dataclass
1112
class GetSrcArtifactVersionSetuptoolsScmHandlerConfig(
@@ -35,11 +36,9 @@ async def run(
3536
run_context: get_src_artifact_version_action.GetSrcArtifactVersionRunContext,
3637
) -> get_src_artifact_version_action.GetSrcArtifactVersionRunResult:
3738
# Use current project if src_artifact_def_path is not provided
38-
src_artifact_def_path = payload.src_artifact_def_path
39-
if src_artifact_def_path is None:
40-
src_artifact_def_path = (
41-
self.project_info_provider.get_current_project_def_path()
42-
)
39+
src_artifact_def_path = resolve_def_path(
40+
payload.src_artifact_def_path, self.project_info_provider
41+
)
4342

4443
src_artifact_raw_def = await self.project_info_provider.get_project_raw_config(
4544
project_def_path=src_artifact_def_path
@@ -54,22 +53,9 @@ async def run(
5453
)
5554

5655
# from setuptools_scm._cli:main
57-
pyproject = src_artifact_def_path.as_posix()
58-
59-
try:
60-
# could be optimized by providing config from project_info_provider instead
61-
# of reading file each time
62-
config = Configuration.from_file(pyproject, root=None)
63-
except (LookupError, FileNotFoundError) as ex:
64-
# no pyproject.toml OR no [tool.setuptools_scm]
65-
self.logger.warning(
66-
f"Warning: could not use {pyproject},"
67-
" using default configuration.\n"
68-
f" Reason: {ex}."
69-
)
70-
config = Configuration(root=src_artifact_def_path.parent.as_posix())
56+
config = load_configuration(src_artifact_def_path, self.logger)
7157

72-
version = _get_version(config)
58+
version = _get_version(config, force_write_version_files=True)
7359
if version is None:
7460
raise code_action.ActionFailedException("ERROR: no version found")
7561

‎extensions/fine_python_setuptools_scm/pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ readme = "README.md"
66
requires-python = ">=3.11"
77
authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }]
88
dependencies = [
9+
"fine_format~=0.1.0a0",
910
"fine_src_artifacts~=0.1.0a0",
1011
"finecode_extension_api~=0.5.0a0",
1112
"setuptools-scm>=8"

‎extensions/fine_python_setuptools_scm/tests/__init__.py‎

Whitespace-only changes.

0 commit comments

Comments
 (0)