Skip to content

Commit ce938a2

Browse files
♻️ Rewrite project initialization to write files from python instead of jinja.
1 parent 8a1078f commit ce938a2

File tree

8 files changed

+118
-84
lines changed

8 files changed

+118
-84
lines changed

docs/index.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ Lapidary-render is a code generator that creates client code from an OpenAPI doc
99

1010
- [x] The generator code should be simple.
1111

12-
Lapidary-render does most of the processing in Python, only leaving the rendering to Jinja templates.
12+
Generator processes data in three stages
13+
14+
1. Enhance and transform OpenAPI to a structure more close resembling python model structure
15+
2. Convert the enhanced OpenAPI to a metamodel
16+
3. Convert the metamodel to a syntax tree
1317

1418
- [x] Client code should be simple.
1519

@@ -61,7 +65,7 @@ origin
6165
: URL of the OpenAPI document, used when document_path is missing, or when `servers` is not defined, or the first server URL is a relative path.
6266

6367
extra_sources
64-
: list of additional source roots for manually written code. The files will be interpreted as templates, but non-template files will also work.
68+
: list of additional source roots for manually written python files.
6569

6670
plugins
6771
: list of plugin classes. See [the section on plug-ins](/plugins)

poetry.lock

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ version = "0.11.1"
1111
packages = [{ include = "lapidary", from = "src" }]
1212
readme = 'Readme.md'
1313
repository = 'https://github.com/python-lapidary/lapidary'
14-
include = [{ path = 'src/lapidary/render/templates' }]
1514
classifiers = [
1615
"Development Status :: 3 - Alpha",
1716
"Environment :: Console",
@@ -41,6 +40,7 @@ pydantic = "^2.5.2"
4140
python-mimeparse = ">=1.6,<3.0"
4241
ruamel-yaml = "^0.18.6"
4342
openapi-pydantic = ">=0.5.0,<0.6.0"
43+
tomli-w = "^1.1.0"
4444

4545
[tool.poetry.scripts]
4646
lapidary = "lapidary.render:app"
@@ -55,7 +55,6 @@ pre-commit = "^4.0.1"
5555
[tool.ruff]
5656
target-version = "py312"
5757
extend-exclude = [
58-
"src/lapidary/render/templates/",
5958
"tests/e2e",
6059
]
6160
line-length = 120
@@ -82,7 +81,6 @@ ignore_missing_imports = true
8281
python_version = "3.12"
8382
packages = ['lapidary.render']
8483
exclude = [
85-
"src/lapidary/render/templates/",
8684
"tests/e2e",
8785
]
8886

src/lapidary/render/main.py

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ async def init_project(
2525
if await project_root.exists():
2626
raise FileExistsError
2727

28+
from .writer import init_project
29+
2830
document_handler = document_handler_for(anyio.Path(), document_path)
2931

3032
if save_document:
@@ -58,32 +60,12 @@ async def init_project(
5860

5961
yaml = ruamel.yaml.YAML(typ='safe')
6062
document = yaml.load(await document_handler.load())
61-
# keep ruff happy
62-
config.__str__()
63-
document.__str__()
64-
65-
# from rybak import TreeTemplate
66-
# from rybak.jinja import JinjaAdapter
67-
#
68-
# TreeTemplate(
69-
# JinjaAdapter(
70-
# jinja2.Environment(
71-
# loader=jinja2.loaders.PackageLoader('lapidary.render', package_path='templates/init'),
72-
# )
73-
# ),
74-
# remove_suffixes=['.jinja'],
75-
# ).render(
76-
# dict(
77-
# get_version=importlib.metadata.version,
78-
# config=config.model_dump(exclude_unset=True, exclude_defaults=True, exclude_none=True),
79-
# document=document,
80-
# ),
81-
# pathlib.Path(project_root),
82-
# )
63+
64+
await init_project(project_root, config, document)
8365

8466

8567
async def render_project(project_root: anyio.Path) -> None:
86-
from .writer import write_all
68+
from .writer import update_project
8769

8870
config = await load_config(project_root)
8971

@@ -102,7 +84,7 @@ async def render_project(project_root: anyio.Path) -> None:
10284
def progress(module: python.AbstractModule) -> None:
10385
progressbar.update(1, module)
10486

105-
await write_all(
87+
await update_project(
10688
model.modules,
10789
project_root / config.extra_sources[0] if config.extra_sources else None,
10890
project_root / 'gen',

src/lapidary/render/pyproject.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from .config import Config
2+
3+
4+
def mk_pyproject_toml(
5+
title: str,
6+
config: Config,
7+
) -> dict:
8+
return {
9+
'build-system': {'build-backend': 'poetry.core.masonry.api', 'requires': ['poetry-core>=1.3.2']},
10+
'tool': {
11+
'poetry': dict(
12+
name=config.package,
13+
description=f'Client library for {title}',
14+
version='0.1.0',
15+
authors=[],
16+
license='',
17+
packages=[
18+
{
19+
'include': config.package,
20+
'from': 'gen',
21+
}
22+
],
23+
dependencies={
24+
'python': '3.9',
25+
'lapidary': '^0.12.0',
26+
},
27+
),
28+
'lapidary': config.model_dump(mode='json', exclude_unset=True, exclude_defaults=True),
29+
},
30+
}

src/lapidary/render/templates/init/.gitignore.jinja

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/lapidary/render/templates/init/pyproject.toml.jinja

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/lapidary/render/writer.py

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,36 @@
11
import logging
22
import shutil
3-
from collections.abc import Callable, Iterable
3+
from collections.abc import Callable, Iterable, Sequence
44

55
import anyio
66
import asyncclick
7+
import libcst as cst
78

9+
from .config import Config
810
from .model import conv_cst, python
911

1012
logger = logging.getLogger(__name__)
1113

1214

13-
async def write_all(
15+
def mk_module(module: python.AbstractModule) -> cst.Module | None:
16+
match module:
17+
case python.SchemaModule():
18+
return conv_cst.mk_schema_module(module)
19+
case python.ClientModule():
20+
return conv_cst.mk_client_module(module)
21+
case python.SecurityModule():
22+
if not module.body:
23+
return None
24+
return conv_cst.mk_security_module(module)
25+
case python.MetadataModule():
26+
return conv_cst.mk_metadata_module(module)
27+
case python.EmptyModule():
28+
return conv_cst.MODULE_EMPTY
29+
case _:
30+
raise TypeError(type(module))
31+
32+
33+
async def update_project(
1434
modules: Iterable[python.AbstractModule],
1535
src_root: anyio.Path | None,
1636
target_root: anyio.Path,
@@ -21,25 +41,13 @@ async def write_all(
2141
written: list[anyio.Path] = []
2242
for module in modules:
2343
update_progress(module)
44+
cst_module = mk_module(module)
45+
if not cst_module:
46+
continue
2447
path = module.path.to_path()
2548
full_path = target_root / path.with_suffix('.py')
26-
match module:
27-
case python.SchemaModule():
28-
code = conv_cst.mk_schema_module(module).code
29-
case python.ClientModule():
30-
code = conv_cst.mk_client_module(module).code
31-
case python.SecurityModule():
32-
if not module.body:
33-
continue
34-
code = conv_cst.mk_security_module(module).code
35-
case python.MetadataModule():
36-
code = conv_cst.mk_metadata_module(module).code
37-
case python.EmptyModule():
38-
code = conv_cst.MODULE_EMPTY.code
39-
case _:
40-
raise TypeError(type(module))
4149
await full_path.parent.mkdir(parents=True, exist_ok=True)
42-
await full_path.write_text(code)
50+
await full_path.write_text(cst_module.code)
4351
written.append(full_path.relative_to(target_root))
4452

4553
root_module_path = anyio.Path(root_package) / '__init__.py'
@@ -50,16 +58,13 @@ async def write_all(
5058
written.append(anyio.Path(root_package, 'py.typed'))
5159

5260
if src_root:
53-
with asyncclick.progressbar(length=0, label='Copying extra sources') as bar:
54-
async for parent, _, files in src_root.walk():
55-
bar.length += len(files)
56-
target_parent = target_root / parent.relative_to(src_root)
57-
await target_parent.mkdir(parents=True, exist_ok=True)
58-
for file in files:
59-
rel_path = parent.relative_to(src_root) / file
60-
bar.update(1, str(rel_path))
61-
shutil.copy(parent / file, target_root / file)
62-
written.append(rel_path)
61+
62+
def cp(src: str, names: list[str]) -> Sequence[str]:
63+
local_src = anyio.Path(src)
64+
written.extend((local_src / name).relative_to(src_root) for name in names)
65+
return ()
66+
67+
shutil.copytree(src_root, target_root, ignore=cp, dirs_exist_ok=True)
6368

6469
with asyncclick.progressbar(length=0, label='Removing stale files') as bar:
6570
async for parent, dirs, files in target_root.walk(False):
@@ -74,3 +79,33 @@ async def write_all(
7479
if not files_ and not dirs:
7580
bar.update(1, str(parent))
7681
await parent.rmdir()
82+
83+
84+
GITIGNORE_LINES = (
85+
'/dist/',
86+
'__pycache__',
87+
)
88+
89+
90+
async def write_gitignore(project_root: anyio.Path):
91+
async with await anyio.open_file(project_root / '.gitignore', 'w') as f:
92+
await f.writelines(GITIGNORE_LINES)
93+
94+
95+
async def write_pyproject(project_root: anyio.Path, title: str, config: Config):
96+
import tomli_w
97+
98+
from .pyproject import mk_pyproject_toml
99+
100+
await (project_root / 'pyproject.toml').write_text(
101+
f"""# This file was generated but won't be updated automatically and may be edited manually.
102+
103+
{tomli_w.dumps(mk_pyproject_toml(title, config))}
104+
"""
105+
)
106+
107+
108+
async def init_project(project_root: anyio.Path, config: Config, raw_document: dict):
109+
await project_root.mkdir(parents=True, exist_ok=True)
110+
await write_pyproject(project_root, raw_document['info']['title'], config)
111+
await write_gitignore(project_root)

0 commit comments

Comments
 (0)