fluent-codegen 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fluent_codegen-0.1.0/LICENSE +13 -0
- fluent_codegen-0.1.0/PKG-INFO +124 -0
- fluent_codegen-0.1.0/README.rst +102 -0
- fluent_codegen-0.1.0/pyproject.toml +84 -0
- fluent_codegen-0.1.0/setup.cfg +4 -0
- fluent_codegen-0.1.0/src/fluent_codegen/__init__.py +1 -0
- fluent_codegen-0.1.0/src/fluent_codegen/ast_compat.py +93 -0
- fluent_codegen-0.1.0/src/fluent_codegen/codegen.py +1653 -0
- fluent_codegen-0.1.0/src/fluent_codegen/py.typed +0 -0
- fluent_codegen-0.1.0/src/fluent_codegen/utils.py +27 -0
- fluent_codegen-0.1.0/src/fluent_codegen.egg-info/PKG-INFO +124 -0
- fluent_codegen-0.1.0/src/fluent_codegen.egg-info/SOURCES.txt +14 -0
- fluent_codegen-0.1.0/src/fluent_codegen.egg-info/dependency_links.txt +1 -0
- fluent_codegen-0.1.0/src/fluent_codegen.egg-info/top_level.txt +1 -0
- fluent_codegen-0.1.0/tests/test_codegen.py +2666 -0
- fluent_codegen-0.1.0/tests/test_fizzbuzz_example.py +70 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2026 Luke Plant
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fluent-codegen
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python library for generating Python code via AST construction.
|
|
5
|
+
Author-email: Luke Plant <luke@lukeplant.me.uk>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/spookylukey/fluent-codegen
|
|
8
|
+
Keywords: codegen,code-generation,ast,python,metaprogramming
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/x-rst
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
fluent-codegen
|
|
24
|
+
==============
|
|
25
|
+
|
|
26
|
+
A Python library for generating Python code via AST construction.
|
|
27
|
+
|
|
28
|
+
Overview
|
|
29
|
+
--------
|
|
30
|
+
|
|
31
|
+
``fluent-codegen`` provides a set of classes that represent simplified
|
|
32
|
+
Python constructs (functions, assignments, expressions, control flow,
|
|
33
|
+
etc.) and can generate real Python ``ast`` nodes. This lets you build
|
|
34
|
+
correct Python code programmatically without manipulating raw AST or
|
|
35
|
+
worrying about string interpolation pitfalls.
|
|
36
|
+
|
|
37
|
+
Originally extracted from
|
|
38
|
+
`fluent-compiler <https://github.com/django-ftl/fluent-compiler>`__,
|
|
39
|
+
where it was used to compile Fluent localization files into Python
|
|
40
|
+
bytecode.
|
|
41
|
+
|
|
42
|
+
Key features
|
|
43
|
+
------------
|
|
44
|
+
|
|
45
|
+
- **Safe by construction** — builds AST, not strings, eliminating
|
|
46
|
+
injection bugs
|
|
47
|
+
- **Scope management** — automatic name deduplication and scope
|
|
48
|
+
tracking
|
|
49
|
+
- **Simplified API** — high-level classes (``Function``, ``If``,
|
|
50
|
+
``Try``, ``StringJoin``, etc.) that map to Python constructs without
|
|
51
|
+
requiring knowledge of the raw ``ast`` module
|
|
52
|
+
- **Security guardrails** — blocks calls to sensitive builtins
|
|
53
|
+
(``exec``, ``eval``, etc.)
|
|
54
|
+
|
|
55
|
+
Installation
|
|
56
|
+
------------
|
|
57
|
+
|
|
58
|
+
.. code:: bash
|
|
59
|
+
|
|
60
|
+
pip install fluent-codegen
|
|
61
|
+
|
|
62
|
+
Requires Python 3.12+.
|
|
63
|
+
|
|
64
|
+
Quick example
|
|
65
|
+
-------------
|
|
66
|
+
|
|
67
|
+
This builds a FizzBuzz function entirely via the codegen API, using
|
|
68
|
+
fluent method-chaining for expressions:
|
|
69
|
+
|
|
70
|
+
.. code:: python
|
|
71
|
+
|
|
72
|
+
from fluent_codegen import codegen
|
|
73
|
+
|
|
74
|
+
# 1. Create a module and a function inside it
|
|
75
|
+
module = codegen.Module()
|
|
76
|
+
func, _ = module.create_function("fizzbuzz", args=["n"])
|
|
77
|
+
|
|
78
|
+
# 2. A Name reference to the "n" parameter (Function *is* a Scope)
|
|
79
|
+
n = codegen.Name("n", func)
|
|
80
|
+
|
|
81
|
+
# 3. Build an if / elif / else chain
|
|
82
|
+
if_stmt = func.body.create_if()
|
|
83
|
+
|
|
84
|
+
# if n % 15 == 0: return "FizzBuzz" — fluent chaining
|
|
85
|
+
branch = if_stmt.create_if_branch(n.mod(codegen.Number(15)).eq(codegen.Number(0)))
|
|
86
|
+
branch.create_return(codegen.String("FizzBuzz"))
|
|
87
|
+
|
|
88
|
+
# elif n % 3 == 0: return "Fizz"
|
|
89
|
+
branch = if_stmt.create_if_branch(n.mod(codegen.Number(3)).eq(codegen.Number(0)))
|
|
90
|
+
branch.create_return(codegen.String("Fizz"))
|
|
91
|
+
|
|
92
|
+
# elif n % 5 == 0: return "Buzz"
|
|
93
|
+
branch = if_stmt.create_if_branch(n.mod(codegen.Number(5)).eq(codegen.Number(0)))
|
|
94
|
+
branch.create_return(codegen.String("Buzz"))
|
|
95
|
+
|
|
96
|
+
# else: return str(n)
|
|
97
|
+
if_stmt.else_block.create_return(codegen.function_call("str", [n], {}, func))
|
|
98
|
+
|
|
99
|
+
# 4. Inspect the generated source
|
|
100
|
+
print(module.as_python_source())
|
|
101
|
+
# def fizzbuzz(n):
|
|
102
|
+
# if n % 15 == 0:
|
|
103
|
+
# return 'FizzBuzz'
|
|
104
|
+
# elif n % 3 == 0:
|
|
105
|
+
# return 'Fizz'
|
|
106
|
+
# elif n % 5 == 0:
|
|
107
|
+
# return 'Buzz'
|
|
108
|
+
# else:
|
|
109
|
+
# return str(n)
|
|
110
|
+
|
|
111
|
+
# 5. Compile, execute, and call the generated function
|
|
112
|
+
code = compile(module.as_ast(), "<fizzbuzz>", "exec")
|
|
113
|
+
ns: dict[str, object] = {}
|
|
114
|
+
exec(code, ns)
|
|
115
|
+
fizzbuzz = ns["fizzbuzz"]
|
|
116
|
+
assert fizzbuzz(15) == "FizzBuzz"
|
|
117
|
+
assert fizzbuzz(9) == "Fizz"
|
|
118
|
+
assert fizzbuzz(10) == "Buzz"
|
|
119
|
+
assert fizzbuzz(7) == "7"
|
|
120
|
+
|
|
121
|
+
License
|
|
122
|
+
-------
|
|
123
|
+
|
|
124
|
+
Apache License 2.0
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
fluent-codegen
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
A Python library for generating Python code via AST construction.
|
|
5
|
+
|
|
6
|
+
Overview
|
|
7
|
+
--------
|
|
8
|
+
|
|
9
|
+
``fluent-codegen`` provides a set of classes that represent simplified
|
|
10
|
+
Python constructs (functions, assignments, expressions, control flow,
|
|
11
|
+
etc.) and can generate real Python ``ast`` nodes. This lets you build
|
|
12
|
+
correct Python code programmatically without manipulating raw AST or
|
|
13
|
+
worrying about string interpolation pitfalls.
|
|
14
|
+
|
|
15
|
+
Originally extracted from
|
|
16
|
+
`fluent-compiler <https://github.com/django-ftl/fluent-compiler>`__,
|
|
17
|
+
where it was used to compile Fluent localization files into Python
|
|
18
|
+
bytecode.
|
|
19
|
+
|
|
20
|
+
Key features
|
|
21
|
+
------------
|
|
22
|
+
|
|
23
|
+
- **Safe by construction** — builds AST, not strings, eliminating
|
|
24
|
+
injection bugs
|
|
25
|
+
- **Scope management** — automatic name deduplication and scope
|
|
26
|
+
tracking
|
|
27
|
+
- **Simplified API** — high-level classes (``Function``, ``If``,
|
|
28
|
+
``Try``, ``StringJoin``, etc.) that map to Python constructs without
|
|
29
|
+
requiring knowledge of the raw ``ast`` module
|
|
30
|
+
- **Security guardrails** — blocks calls to sensitive builtins
|
|
31
|
+
(``exec``, ``eval``, etc.)
|
|
32
|
+
|
|
33
|
+
Installation
|
|
34
|
+
------------
|
|
35
|
+
|
|
36
|
+
.. code:: bash
|
|
37
|
+
|
|
38
|
+
pip install fluent-codegen
|
|
39
|
+
|
|
40
|
+
Requires Python 3.12+.
|
|
41
|
+
|
|
42
|
+
Quick example
|
|
43
|
+
-------------
|
|
44
|
+
|
|
45
|
+
This builds a FizzBuzz function entirely via the codegen API, using
|
|
46
|
+
fluent method-chaining for expressions:
|
|
47
|
+
|
|
48
|
+
.. code:: python
|
|
49
|
+
|
|
50
|
+
from fluent_codegen import codegen
|
|
51
|
+
|
|
52
|
+
# 1. Create a module and a function inside it
|
|
53
|
+
module = codegen.Module()
|
|
54
|
+
func, _ = module.create_function("fizzbuzz", args=["n"])
|
|
55
|
+
|
|
56
|
+
# 2. A Name reference to the "n" parameter (Function *is* a Scope)
|
|
57
|
+
n = codegen.Name("n", func)
|
|
58
|
+
|
|
59
|
+
# 3. Build an if / elif / else chain
|
|
60
|
+
if_stmt = func.body.create_if()
|
|
61
|
+
|
|
62
|
+
# if n % 15 == 0: return "FizzBuzz" — fluent chaining
|
|
63
|
+
branch = if_stmt.create_if_branch(n.mod(codegen.Number(15)).eq(codegen.Number(0)))
|
|
64
|
+
branch.create_return(codegen.String("FizzBuzz"))
|
|
65
|
+
|
|
66
|
+
# elif n % 3 == 0: return "Fizz"
|
|
67
|
+
branch = if_stmt.create_if_branch(n.mod(codegen.Number(3)).eq(codegen.Number(0)))
|
|
68
|
+
branch.create_return(codegen.String("Fizz"))
|
|
69
|
+
|
|
70
|
+
# elif n % 5 == 0: return "Buzz"
|
|
71
|
+
branch = if_stmt.create_if_branch(n.mod(codegen.Number(5)).eq(codegen.Number(0)))
|
|
72
|
+
branch.create_return(codegen.String("Buzz"))
|
|
73
|
+
|
|
74
|
+
# else: return str(n)
|
|
75
|
+
if_stmt.else_block.create_return(codegen.function_call("str", [n], {}, func))
|
|
76
|
+
|
|
77
|
+
# 4. Inspect the generated source
|
|
78
|
+
print(module.as_python_source())
|
|
79
|
+
# def fizzbuzz(n):
|
|
80
|
+
# if n % 15 == 0:
|
|
81
|
+
# return 'FizzBuzz'
|
|
82
|
+
# elif n % 3 == 0:
|
|
83
|
+
# return 'Fizz'
|
|
84
|
+
# elif n % 5 == 0:
|
|
85
|
+
# return 'Buzz'
|
|
86
|
+
# else:
|
|
87
|
+
# return str(n)
|
|
88
|
+
|
|
89
|
+
# 5. Compile, execute, and call the generated function
|
|
90
|
+
code = compile(module.as_ast(), "<fizzbuzz>", "exec")
|
|
91
|
+
ns: dict[str, object] = {}
|
|
92
|
+
exec(code, ns)
|
|
93
|
+
fizzbuzz = ns["fizzbuzz"]
|
|
94
|
+
assert fizzbuzz(15) == "FizzBuzz"
|
|
95
|
+
assert fizzbuzz(9) == "Fizz"
|
|
96
|
+
assert fizzbuzz(10) == "Buzz"
|
|
97
|
+
assert fizzbuzz(7) == "7"
|
|
98
|
+
|
|
99
|
+
License
|
|
100
|
+
-------
|
|
101
|
+
|
|
102
|
+
Apache License 2.0
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fluent-codegen"
|
|
3
|
+
description = "A Python library for generating Python code via AST construction."
|
|
4
|
+
license = "Apache-2.0"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Luke Plant", email = "luke@lukeplant.me.uk" }
|
|
7
|
+
]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
keywords = [
|
|
10
|
+
"codegen",
|
|
11
|
+
"code-generation",
|
|
12
|
+
"ast",
|
|
13
|
+
"python",
|
|
14
|
+
"metaprogramming",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
24
|
+
"Topic :: Software Development :: Code Generators",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
dynamic = ["version"]
|
|
28
|
+
|
|
29
|
+
dependencies = []
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Repository = "https://github.com/spookylukey/fluent-codegen"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
[project.readme]
|
|
36
|
+
file = "README.rst"
|
|
37
|
+
content-type = "text/x-rst"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["setuptools>=61.2"]
|
|
41
|
+
build-backend = "setuptools.build_meta"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools]
|
|
44
|
+
package-dir = {"" = "src"}
|
|
45
|
+
include-package-data = false
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["src"]
|
|
49
|
+
namespaces = false
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.dynamic]
|
|
52
|
+
version = {attr = "fluent_codegen.__version__"}
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 120
|
|
56
|
+
target-version = "py312"
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
ignore = ["E501", "E731"]
|
|
60
|
+
extend-select = [
|
|
61
|
+
"UP",
|
|
62
|
+
"I",
|
|
63
|
+
"FLY",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint.isort]
|
|
67
|
+
known-first-party = ["fluent_codegen"]
|
|
68
|
+
|
|
69
|
+
[tool.pytest.ini_options]
|
|
70
|
+
testpaths = ["tests"]
|
|
71
|
+
addopts = "--cov=fluent_codegen --cov-fail-under=100"
|
|
72
|
+
|
|
73
|
+
[dependency-groups]
|
|
74
|
+
dev = [
|
|
75
|
+
"hypothesis>=4.9.0",
|
|
76
|
+
"pre-commit>=4.5.1",
|
|
77
|
+
"pyright==1.1.406",
|
|
78
|
+
"pytest>=7.4.4",
|
|
79
|
+
"pytest-cov>=7.0.0",
|
|
80
|
+
"ruff>=0.4.0",
|
|
81
|
+
"tox>=4.36.0",
|
|
82
|
+
"tox-uv>=1.29.0",
|
|
83
|
+
]
|
|
84
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Compatibility module for generating Python AST.
|
|
2
|
+
|
|
3
|
+
Provides a curated subset of the stdlib `ast` module used by the codegen module.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
from typing import TypedDict
|
|
8
|
+
|
|
9
|
+
# This is a very limited subset of Python AST:
|
|
10
|
+
# - only the things needed by codegen.py
|
|
11
|
+
|
|
12
|
+
Add = ast.Add
|
|
13
|
+
And = ast.And
|
|
14
|
+
Assign = ast.Assign
|
|
15
|
+
AnnAssign = ast.AnnAssign
|
|
16
|
+
BoolOp = ast.BoolOp
|
|
17
|
+
BinOp = ast.BinOp
|
|
18
|
+
Compare = ast.Compare
|
|
19
|
+
Dict = ast.Dict
|
|
20
|
+
Div = ast.Div
|
|
21
|
+
Eq = ast.Eq
|
|
22
|
+
ExceptHandler = ast.ExceptHandler
|
|
23
|
+
Expr = ast.Expr
|
|
24
|
+
FloorDiv = ast.FloorDiv
|
|
25
|
+
Gt = ast.Gt
|
|
26
|
+
GtE = ast.GtE
|
|
27
|
+
If = ast.If
|
|
28
|
+
In = ast.In
|
|
29
|
+
List = ast.List
|
|
30
|
+
Load = ast.Load
|
|
31
|
+
Lt = ast.Lt
|
|
32
|
+
LtE = ast.LtE
|
|
33
|
+
Mod = ast.Mod
|
|
34
|
+
Module = ast.Module
|
|
35
|
+
Mult = ast.Mult
|
|
36
|
+
MatMult = ast.MatMult
|
|
37
|
+
NotEq = ast.NotEq
|
|
38
|
+
NotIn = ast.NotIn
|
|
39
|
+
Or = ast.Or
|
|
40
|
+
Pow = ast.Pow
|
|
41
|
+
Sub = ast.Sub
|
|
42
|
+
boolop = ast.boolop
|
|
43
|
+
cmpop = ast.cmpop
|
|
44
|
+
operator = ast.operator
|
|
45
|
+
Pass = ast.Pass
|
|
46
|
+
Return = ast.Return
|
|
47
|
+
Set = ast.Set
|
|
48
|
+
Starred = ast.Starred
|
|
49
|
+
Store = ast.Store
|
|
50
|
+
Subscript = ast.Subscript
|
|
51
|
+
Tuple = ast.Tuple
|
|
52
|
+
arguments = ast.arguments
|
|
53
|
+
JoinedStr = ast.JoinedStr
|
|
54
|
+
FormattedValue = ast.FormattedValue
|
|
55
|
+
Attribute = ast.Attribute
|
|
56
|
+
Call = ast.Call
|
|
57
|
+
FunctionDef = ast.FunctionDef
|
|
58
|
+
Name = ast.Name
|
|
59
|
+
Try = ast.Try
|
|
60
|
+
With = ast.With
|
|
61
|
+
withitem = ast.withitem
|
|
62
|
+
arg = ast.arg
|
|
63
|
+
keyword = ast.keyword
|
|
64
|
+
ClassDef = ast.ClassDef
|
|
65
|
+
walk = ast.walk
|
|
66
|
+
fix_missing_locations = ast.fix_missing_locations
|
|
67
|
+
unparse = ast.unparse
|
|
68
|
+
Constant = ast.Constant
|
|
69
|
+
AST = ast.AST
|
|
70
|
+
stmt = ast.stmt
|
|
71
|
+
expr = ast.expr
|
|
72
|
+
Import = ast.Import
|
|
73
|
+
ImportFrom = ast.ImportFrom
|
|
74
|
+
alias = ast.alias
|
|
75
|
+
|
|
76
|
+
# `compile` builtin needs these attributes on AST nodes.
|
|
77
|
+
# It's hard to get something sensible we can put for line/col numbers so we put arbitrary values.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DefaultAstArgs(TypedDict):
|
|
81
|
+
lineno: int
|
|
82
|
+
col_offset: int
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
DEFAULT_AST_ARGS: DefaultAstArgs = {"lineno": 1, "col_offset": 1}
|
|
86
|
+
# Some AST types have different requirements:
|
|
87
|
+
DEFAULT_AST_ARGS_MODULE: dict[str, object] = dict()
|
|
88
|
+
DEFAULT_AST_ARGS_ADD: dict[str, object] = dict()
|
|
89
|
+
DEFAULT_AST_ARGS_ARGUMENTS: dict[str, object] = dict()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def subscript_slice_object[T](value: T) -> T:
|
|
93
|
+
return value
|