1{
2 lib,
3 buildPythonPackage,
4 setuptools,
5 pytestCheckHook,
6 tree-sitter,
7 symlinkJoin,
8 writeTextDir,
9 pythonOlder,
10 # `name`: grammar derivation pname in the format of `tree-sitter-<lang>`
11 name,
12 grammarDrv,
13}:
14let
15 inherit (grammarDrv) version;
16
17 snakeCaseName = lib.replaceStrings [ "-" ] [ "_" ] name;
18 drvPrefix = "python-${name}";
19 # If the name of the grammar attribute differs from the grammar's symbol name,
20 # it could cause a symbol mismatch at load time. This manually curated collection
21 # of overrides ensures the binding can find the correct symbol
22 langIdentOverrides = {
23 tree_sitter_org_nvim = "tree_sitter_org";
24 };
25 langIdent = langIdentOverrides.${snakeCaseName} or snakeCaseName;
26in
27buildPythonPackage {
28 inherit version;
29 pname = drvPrefix;
30 pyproject = true;
31 build-system = [ setuptools ];
32
33 src = symlinkJoin {
34 name = "${drvPrefix}-source";
35 paths = [
36 (writeTextDir "${snakeCaseName}/__init__.py" ''
37 # AUTO-GENERATED DO NOT EDIT
38
39 # preload the parser object before importing c binding
40 # this way we can avoid dynamic linker kicking in when
41 # downstream code imports this python module
42 import ctypes
43 import sys
44 import os
45 parser = "${grammarDrv}/parser"
46 try:
47 ctypes.CDLL(parser, mode=ctypes.RTLD_GLOBAL) # cached
48 except OSError as e:
49 raise ImportError(f"cannot load tree-sitter parser object from {parser}: {e}")
50
51 # expose binding
52 from ._binding import language
53 __all__ = ["language"]
54 '')
55 (writeTextDir "${snakeCaseName}/binding.c" ''
56 // AUTO-GENERATED DO NOT EDIT
57
58 #include <Python.h>
59
60 typedef struct TSLanguage TSLanguage;
61
62 TSLanguage *${langIdent}(void);
63
64 static PyObject* _binding_language(PyObject *self, PyObject *args) {
65 return PyLong_FromVoidPtr(${langIdent}());
66 }
67
68 static PyMethodDef methods[] = {
69 {"language", _binding_language, METH_NOARGS,
70 "Get the tree-sitter language for this grammar."},
71 {NULL, NULL, 0, NULL}
72 };
73
74 static struct PyModuleDef module = {
75 .m_base = PyModuleDef_HEAD_INIT,
76 .m_name = "_binding",
77 .m_doc = NULL,
78 .m_size = -1,
79 .m_methods = methods
80 };
81
82 PyMODINIT_FUNC PyInit__binding(void) {
83 return PyModule_Create(&module);
84 }
85 '')
86 (writeTextDir "setup.py" ''
87 # AUTO-GENERATED DO NOT EDIT
88
89 from platform import system
90 from setuptools import Extension, setup
91
92
93 setup(
94 packages=["${snakeCaseName}"],
95 ext_package="${snakeCaseName}",
96 ext_modules=[
97 Extension(
98 name="_binding",
99 sources=["${snakeCaseName}/binding.c"],
100 extra_objects = ["${grammarDrv}/parser"],
101 extra_compile_args=(
102 ["-std=c11"] if system() != 'Windows' else []
103 ),
104 )
105 ],
106 )
107 '')
108 (writeTextDir "pyproject.toml" ''
109 # AUTO-GENERATED DO NOT EDIT
110
111 [build-system]
112 requires = ["setuptools", "wheel"]
113 build-backend = "setuptools.build_meta"
114
115 [project]
116 name="${snakeCaseName}"
117 description = "${langIdent} grammar for tree-sitter"
118 version = "${version}"
119 keywords = ["parsing", "incremental", "python"]
120 classifiers = [
121 "Development Status :: 4 - Beta",
122 "Intended Audience :: Developers",
123 "Topic :: Software Development :: Compilers",
124 "Topic :: Text Processing :: Linguistic",
125 ]
126
127 requires-python = ">=3.8"
128 license = "MIT"
129 readme = "README.md"
130
131 [project.optional-dependencies]
132 core = ["tree-sitter~=0.21"]
133
134 [tool.cibuildwheel]
135 build = "cp38-*"
136 build-frontend = "build"
137 '')
138 (writeTextDir "tests/test_language.py" ''
139 # AUTO-GENERATED DO NOT EDIT
140
141 from ${snakeCaseName} import language
142 from tree_sitter import Language, Parser
143
144 # This test only checks that the binding can load the grammar from the compiled shared object.
145 # It does not verify the grammar itself; that is tested in
146 # `pkgs/development/tools/parsing/tree-sitter/grammar.nix`.
147
148 def test_language():
149 lang = Language(language())
150 assert lang is not None
151 parser = Parser(lang)
152 tree = parser.parse(bytes("", "utf-8"))
153 assert tree is not None
154 '')
155 ];
156 };
157
158 preCheck = ''
159 # https://github.com/NixOS/nixpkgs/issues/255262
160 rm -r ${snakeCaseName}
161 '';
162
163 disabled = pythonOlder "3.8";
164
165 nativeCheckInputs = [
166 tree-sitter
167 pytestCheckHook
168 ];
169
170 pythonImportsCheck = [ snakeCaseName ];
171
172 meta = {
173 description = "Python bindings for ${name}";
174 license = lib.licenses.mit;
175 maintainers = with lib.maintainers; [
176 a-jay98
177 adfaure
178 mightyiam
179 stepbrobd
180 ];
181 };
182}