Repository navigation
Expand file tree
/
Copy pathpyproject.toml
More file actions
512 lines (481 loc) · 20.6 KB
/
Copy pathpyproject.toml
File metadata and controls
512 lines (481 loc) · 20.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
[project]
name = "maptasker"
version = "15.0.1"
description = "Utility to display/edit/analyze your entire Android 'Tasker' configuration in your browser."
authors = [
{ name = "Michael Rubin", email = "mikrubin@gmail.com" },
]
license = {text = "MIT"}
readme = "README_PyPl.md"
requires-python = ">=3.11, <4.0"
keywords = [
"tasker",
"Tasker",
"map tasker",
"configuration",
"tasker configuration",
]
classifiers = [
# "Development Status :: 1 - Planning",
# "Development Status :: 2 - Pre-Alpha",
# "Development Status :: 3 - Alpha",
# "Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
# "Development Status :: 6 - Mature",
# "Development Status :: 7 - Inactive",
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
'darkdetect>=0.8.0',
'defusedxml>=0.7.1',
# aiutils imports it directly. NiceGUI happens to bring it in, but that is not a promise.
'httpx>=0.28.1',
'keyring>=25.7.0',
'nicegui>=3.18.0',
'packaging>=26.3',
'pillow>=13.0.0',
"platformdirs>=4.13.5",
'requests>=2.34.2',
'tomli_w>=1.2.0',
# Windows has no system time-zone database, so zoneinfo (maputils' time-zone lookup) finds no
# zone at all without this and silently falls back to UTC. Linux and macOS ship their own.
"tzdata>=2026.5; sys_platform == 'win32'",
]
[project.optional-dependencies]
# Only the AI analysis features (and the AI question box) use these. MapTasker never installs
# them itself: `pip install "maptasker[ai]"` (or `uv add "maptasker[ai]"`) is how they arrive,
# and maputil3.AI_EXTRA_INSTALL_COMMAND is what the program tells the user to run.
ai = [
'anthropic>=1.16.0',
'google-genai>=2.31.0',
'ollama>=0.6.3',
'openai>=3.35.1',
]
[project.urls]
Homepage = "https://github.com/mctinker/Map-Tasker"
"Bug Tracker" = "https://github.com/mctinker/Map-Tasker/issues"
Changelog = "https://github.com/mctinker/Map-Tasker/CHANGELOG.md"
[project.scripts]
maptasker = "maptasker.main:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.sdist]
include = [
"LICENSE",
"README_PyPl.md",
"maptasker/**/*",
"pyproject.toml",
"sample.prj.xml",
]
# Explicitly exclude hidden files, virtual environments, and config files
exclude = [
"/.venv",
"/.vscode",
"/.github",
"/tests",
"/XML",
"/*.lock",
"/.gitignore",
"/docs",
".claude",
]
[tool.uv]
# `uv sync` installs these and no others: the development tools, without the AI libraries.
# ai-dev and translate (dependency-groups, below) are opt-in with --group.
default-groups = ["dev"]
[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple"
publish-url = "https://test.pypi.org/legacy/"
explicit = true
[tool.hatch.build.targets.wheel]
# Ensure only the maptasker folder is treated as the package
packages = ["maptasker"]
[dependency-groups]
test = [
"pytest>=9.1.1",
"pytest-mock>=3.17.0",
"pytest-asyncio>=1.4.0",
"pytest-cov>=7.1.0",
]
# 'black' was removed: [tool.ruff.format] is the formatter for this project, and
# running both against the same files makes them fight over the same lines.
dev = [
# The test group, once, rather than a second copy of it that can drift.
{include-group = "test"},
"deep-translator>=1.11.4",
"import-linter>=2.15",
"matchify>=0.3.0",
"pip-autoremove>=0.10.0",
"pyright[nodejs]>=1.1.414",
"pytest>=9.1.1",
"rich>=15.0.0",
"ruff>=0.17.1",
"vulture>=2.16",
]
# Groups that a plain `uv sync` does NOT install (see default-groups under [tool.uv]).
# MapTasker's AI libraries are the optional "ai" extra, and nothing here should bring them
# into an ordinary development environment.
#
# The AI libraries themselves, for whoever works on the AI features or runs the type check
# (which resolves the imports in aiutils.py, mapai.py and mapask.py): uv sync --group ai-dev
ai-dev = [
"maptasker[ai]",
]
# ai-translator, which pulls in openai. Nothing in the repository imports it.
translate = [
"ai-translator>=0.1.0",
]
[tool.ruff]
include = ["pyproject.toml", "maptasker/main.py", "maptasker/src/**/*.py", "scripts/**/*.py"]
respect-gitignore = true
line-length = 120
show-fixes = true
src = ['src',]
# editor.formatOnSaveMode = "modificationsIfAvailable"
[tool.ruff.lint]
# The project's one logger lives in sysconst and is imported everywhere else. Naming it
# here lets ruff see that 'logger.exception(...)' records the traceback -- without it, a
# broad except that does log its failure still trips BLE001.
logger-objects = ["maptasker.src.core.sysconst.logger"]
# PLW1514 (open() with no encoding=) is a preview rule. On Windows the default encoding is
# the locale's (cp1252), not UTF-8, so an open() with no encoding garbles or rejects a
# non-ASCII Tasker name there and works fine on a Mac -- the kind of bug a Mac never shows.
# explicit-preview-rules keeps every OTHER preview rule off: a preview rule is enabled only
# when it is named in full below, never by a prefix such as 'PL'.
preview = true
explicit-preview-rules = true
select = [
'A', # Builtins
'ANN', # Annotations
'ARG', # Unused arguments
'B', # Bugbear
'BLE', # Blind except
'C4', # Comprehensions
'C90', # mccabe
'COM', # Commas
'D1', # Undocumented public elements
# 'D2', # Docstring conventions
'D3', # Triple double quotes
# 'D4', # Docstring text format
'DTZ', # Datetimes
'EM', # Error messages
# 'ERA', # Commented-out code
# 'EXE', # Executable
'F', # Pyflakes
'FA', # __future__ annotations
'FLY', # F-strings
# 'FURB', # Refurb
'G', # Logging format
'I', # Isort
'ICN', # Import conventions
'INP', # Disallow PEP-420 (Implicit namespace packages)
'INT', # gettext
'ISC', # Implicit str concat
# 'LOG', # Logging
'N', # PEP-8 Naming
'NPY', # Numpy
'PERF', # Unnecessary performance costs
'PGH', # Pygrep hooks
'PIE', # Unnecessary code
'PL', # Pylint
'PLW1514', # open() without an explicit encoding (a preview rule; see explicit-preview-rules)
'PT', # Pytest
# 'PTH', # Use Pathlib
'PYI', # Stub files
'Q', # Quotes
'RET', # Return
'RUF', # Ruff
'RSE', # Raise
'S', # Bandit
'SIM', # Code simplification
'SLF', # Private member access
'SLOT', # __slots__
'T10', # Debugger
'T20', # Print
'TCH', # Type checking
'TID', # Tidy imports
'TRY', # Exception handling
'UP', # Pyupgrade
'W', # Warnings
'YTT', # sys.version
]
ignore = [
"PLR2004", # Constant value comparison
"SIM115", # Missing "with" on open file
"S606", # No shell
"B009", # Do not perform function calls in argument defaults
# -- House style, not debt -------------------------------------------------
# This project imports ElementTree as 'ET' (and as 'ETW' where a writer-side
# alias is wanted). Ruff wants one specific alias and objects to the casing of
# both. 16 occurrences, all deliberate.
"ICN001", # unconventional-import-alias
"N814", # camelcase-imported-as-constant
"N817", # camelcase-imported-as-acronym
# The logger is written to with f-strings (100+ calls), and an EXPECTED failure --
# an OSError writing a report, say -- is logged as one line with logger.error, not
# as a traceback. Both rules only woke up when logger-objects (above) let ruff
# recognise the logger at all; nothing they report was newly introduced.
"G004", # logging-f-string
"TRY400", # error-instead-of-exception
# -- Formatter conflict ----------------------------------------------------
# Ruff emits a startup warning when COM812 is selected alongside its formatter,
# because the two disagree about trailing commas. The formatter already adds
# them, so the lint rule is redundant as well as conflicting.
"COM812", # missing-trailing-comma
]
[tool.ruff.lint.pycodestyle]
ignore-overlong-task-comments = true
[tool.ruff.lint.flake8-quotes]
docstring-quotes = 'double'
multiline-quotes = 'double'
[tool.ruff.lint.mccabe]
# A ceiling, and as low as the code allows: the worst function sits at it. The target for new code
# is lower; see [tool.complexity-ratchet] below and scripts/complexity_ratchet.py.
max-complexity = 15
[tool.ruff.lint.per-file-ignores]
# https://docs.astral.sh/ruff/rules/
'__init__.py' = ['F401','F403','F405',]
'tests/*' = ['ANN', 'ARG', 'INP001', 'S101',]
# scripts/ holds maintainer command-line tools, not a package, and they report by printing.
'scripts/*' = ['INP001', 'T201']
# capture_screens.py imports MapTasker inside its functions on purpose: serve() puts the chosen
# checkout on sys.path (--root) and checks the keyring and HOME are isolated before anything of
# MapTasker's is loaded, and playwright is only there for the driver half of the one file.
# S603: the one subprocess it starts is this same script, from the project's own interpreter.
'scripts/capture_screens.py' = ['PLC0415', 'S603']
# complexity_ratchet.py runs ruff to measure the functions, and coverage_ratchet.py runs coverage,
# each from the project's own interpreter.
'scripts/complexity_ratchet.py' = ['S603']
'scripts/coverage_ratchet.py' = ['S603']
# There is no per-file backlog any more. Every rule is enforced in every file; a new
# violation fails CI, and the fix belongs in the code rather than in a line added here.
# What is exempted above is exempted on purpose (tests, scripts, __init__.py), with the reason.
[tool.ruff.lint.pylint]
# Ceilings, not targets: each is as low as the code allows, so the worst function in the
# project sits right at it and a function any worse than that fails. They were 15, 20, 10
# and 120 until the largest functions were split up. What new code is held to is lower still:
# see [tool.complexity-ratchet] below and scripts/complexity_ratchet.py, which also keeps the
# functions that are over it from growing.
max-args = 9
max-branches = 15
max-returns = 9
max-statements = 60
max-locals = 20
max-nested-blocks = 10
# The targets scripts/complexity_ratchet.py holds every function to, as opposed to the ceilings
# above, which nothing may pass. A function over a target is listed, with how far over it is,
# in scripts/complexity_baseline.json; that list can only shrink. Fix a function and lower it
# with `uv run python scripts/complexity_ratchet.py --update`.
[tool.complexity-ratchet]
statements = 50 # ruff PLR0915, the pylint default
branches = 12 # ruff PLR0912, the pylint default
complexity = 12 # ruff C901
arguments = 8 # ruff PLR0913
lines = 100 # first line of the def to its last, which ruff has no rule for
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = 'all'
[tool.ruff.format]
quote-style = "double"
# indent-style = "tab"
docstring-code-format = true
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
# Stable formatter style. Ruff's preview style is unstable by design -- it can
# change in any minor release -- which is a poor fit for the enforced
# `ruff format --check` gate in CI. Its only visible effect on this project was
# to break the first call of a method chain onto its own line, leaving a bare
# receiver ('ui', 'clean_text') stranded above it.
preview = false
# NOTE: a local, git-ignored pytest.ini used to shadow this block entirely --
# pytest.ini outranks pyproject.toml, so these settings never took effect on a
# developer machine and CI (which has no pytest.ini, it is not in the repo) would
# have run under different settings. This block is now the single source of truth;
# delete any leftover local pytest.ini so both run the same way.
[tool.pytest.ini_options]
pythonpath = ["."]
testpaths = ["tests"]
# 52 tests are async and rely on the @pytest.mark.asyncio marker.
asyncio_mode = "strict"
asyncio_default_fixture_loop_scope = "function"
[tool.coverage.run]
source = ["maptasker"]
branch = true
[tool.coverage.report]
show_missing = true
skip_covered = true
# Set from what CI measures: a checkout with no XML/, since XML/ holds personal Tasker
# data and is not in the repository. Every test that reads sample data reads
# tests/data/synthetic_backup.xml, with any backups in XML/ only as extra material, so a
# developer machine measures the same as CI, with or without XML/. The floor is about two
# points under what is measured, so a per-Python-version difference cannot fail the build
# while a real regression does.
#
# Raise this as coverage improves -- it is meant to ratchet upward, never down.
#
# This is one number for the whole program, which lets a well-tested layer cover for a
# neglected one. scripts/coverage_ratchet.py holds each layer (gui, core, mapgen ...) to its
# own floor in scripts/coverage_baseline.json as well; raise those with --update.
#
# History: 36 at 37.58% (754 tests) -> 44 at 45.41%, when the XML-parsing core
# (taskerd/tasks/profiles/projects/scenes/action/actargs) gained direct tests and
# went from 43% to 75% between them -> 42, recalibrated to CI's environment while
# tests still skipped without XML/ -> 56 at 57.5% (1847 tests), once they no longer did
# -> 60 at 62.25% (2300 tests), once the installer, backup fetch, Preferences, Task flags and the
# Fix, Restore and Impact dialogs had direct tests -> 66 at 68.5% (3114 tests), once the Profile, Project and
# Scene editors, label formatting, the AI analysis, the command-line setup and the small utilities did
# -> 73 at 75.8% (3507 tests), once the Edit Profile dialogs, the Find/Replace and Search dialogs and the
# Legacy and Version 2 Scene designers were driven through NiceGUI's user simulation
# -> 78 at 80.6% (3788 tests), once the editor event handlers and the Map/Diagram/Scene-preview/Tree views were too.
# -> 83 at 85.2% (4750 tests), once the start-up, crash handling, safety copies, AI model lists, condition and
# action wording, and the GUI's plain logic and navigation had direct tests.
# Set about two points under, as before.
fail_under = 83
exclude_also = [
"if TYPE_CHECKING:",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"@(abc\\.)?abstractmethod",
]
# Checks that the annotations ruff's ANN rules require are actually right. Enforced by
# scripts/typecheck_ratchet.py against scripts/pyright_baseline.json, so only new errors fail.
[tool.pyright]
include = ["maptasker/main.py", "maptasker/src", "scripts"]
typeCheckingMode = "basic"
# Strict mode, file by file. Basic mode is what the baseline ratchets (see above); these files are
# clean under strict as well, and tests/test_strict_typing.py keeps it that way for new ones: every
# module in maptasker/src is either listed here or in scripts/strict_exempt.json, and a new module
# may not be added to the second list -- it has to be written clean enough to be listed here.
# Moving a module out of the exempt list into this one is the whole of paying one off. (A module
# whose dicts others index stays out, servicec for one: strict infers them more narrowly than basic.)
strict = [
"maptasker/src/__init__.py",
"maptasker/src/analysis/__init__.py",
"maptasker/src/analysis/livemon.py",
"maptasker/src/analysis/resrefs.py",
"maptasker/src/core/__init__.py",
"maptasker/src/core/actiont.py",
"maptasker/src/core/assettext.py",
"maptasker/src/core/caches.py",
"maptasker/src/core/clock.py",
"maptasker/src/core/config.py",
"maptasker/src/core/deprecate.py",
"maptasker/src/core/error.py",
"maptasker/src/core/maputil3.py",
"maptasker/src/core/mtexcept.py",
"maptasker/src/core/nameattr.py",
"maptasker/src/core/outdir.py",
"maptasker/src/core/presave.py",
"maptasker/src/core/resstore.py",
"maptasker/src/core/taskervars.py",
"maptasker/src/core/translator.py",
"maptasker/src/device/__init__.py",
"maptasker/src/device/devicecheck.py",
"maptasker/src/device/devicefiles.py",
"maptasker/src/device/deviceimport.py",
"maptasker/src/device/devicemonitor.py",
"maptasker/src/device/deviceresource.py",
"maptasker/src/edit/__init__.py",
"maptasker/src/edit/taskedit_model.py",
"maptasker/src/gui/__init__.py",
"maptasker/src/gui/guiwins_impact.py",
"maptasker/src/gui/guiwins_monitor.py",
"maptasker/src/gui/guiwins_resources.py",
"maptasker/src/gui/userhelp.py",
"maptasker/src/gui/webassets.py",
"maptasker/src/mapgen/__init__.py",
"maptasker/src/mapgen/diagcnst.py",
"maptasker/src/mapops/__init__.py",
"maptasker/src/mapops/maprefac_variable.py",
"maptasker/src/mapops/mapswap.py",
"maptasker/src/progargs.py",
]
# The oldest Python requires-python allows, so code that needs a newer one is an error.
pythonVersion = "3.11"
# Fixed rather than taken from the machine it runs on: platform-specific modules (winreg)
# check differently per OS, and the baseline has to count the same on a Mac as in CI.
pythonPlatform = "Linux"
venvPath = "."
venv = ".venv"
# Import layering, checked by import-linter ('lint-imports') in CI. The source is split into
# sub-packages of maptasker/src, each layer allowed to import only the ones below it:
# entry points -- mapit, progargs, runcli, rungui, clireports: start the program, CLI or GUI
# gui -- NiceGUI windows, dialogs and their event handlers
# device -- what is done on the Android device, through Tasker's HTTP server
# mapops -- bulk replace, refactor, repair, restore, draft; the TaskerNet and readiness tools
# analysis -- read-only questions asked of a configuration
# mapgen : edit -- loading a backup and building the Map from it; the Task/Profile/Project/Scene
# editors and Undo. Siblings: each needs a little of the other.
# core -- run state, the Tasker action tables, shared utilities
# so a GUI module cannot be pulled into the core by accident, the core stays usable by the CLI
# and the tests without a web server, and the Map builder never reaches up into an operation.
# Every module in maptasker.src must be in exactly one layer ("exhaustive"): a new package fails
# the check until it is placed. Import cycles, and the imports deferred into functions to break
# them, are ratcheted separately by tests/test_import_structure.py.
[tool.importlinter]
root_package = "maptasker"
include_external_packages = true
# An import under "if TYPE_CHECKING:" never runs, so it can neither pull a module in at
# runtime nor close a cycle -- it is the sanctioned way to name a type from a module that
# must not be imported for real.
exclude_type_checking_imports = true
[[tool.importlinter.contracts]]
name = "Entry points above the GUI, the GUI above the operations, the operations above the core"
type = "layers"
containers = ["maptasker.src"]
exhaustive = true
layers = [
"mapit : progargs : runcli : rungui : clireports",
"gui",
"device",
"mapops",
"analysis",
"mapgen : edit",
"core",
]
# A Scene is one of two kinds, and what a caller does with one should not depend on which.
# scenemodel is where that is decided (see its header): the analyses, the editing core
# (sceneedit), the Map output (scenes) and the bulk replace (mapswap) all ask it. What is
# left importing a kind's editor directly is the code that edits that kind and nothing else --
# its designer, the property dialogs and the preview -- and this keeps it that list.
[[tool.importlinter.contracts]]
name = "Only the Legacy Scene designer and its dialogs import the Legacy Scene editor"
type = "protected"
protected_modules = ["maptasker.src.edit.sceneedit_legacy"]
allowed_importers = [
"maptasker.src.gui.guiwins",
"maptasker.src.gui.guiwins_designer_legacy",
"maptasker.src.gui.guiwins_legacyarg",
"maptasker.src.gui.guiwins_sceneprops",
"maptasker.src.edit.scenemodel",
"maptasker.src.gui.userintr_editors",
]
[[tool.importlinter.contracts]]
name = "Only the Version 2 Scene designer and its preview import the Version 2 Scene editor"
type = "protected"
protected_modules = ["maptasker.src.edit.sceneedit_v2"]
allowed_importers = [
"maptasker.src.gui.guiwins_designer_v2",
"maptasker.src.gui.guiwins_views",
"maptasker.src.edit.scenemodel",
]
[[tool.importlinter.contracts]]
name = "Only the GUI and the entry points import NiceGUI"
type = "protected"
protected_modules = ["nicegui"]
allowed_importers = [
"maptasker.src.mapit",
"maptasker.src.progargs",
"maptasker.src.runcli",
"maptasker.src.rungui",
"maptasker.src.gui",
]
# restart_program_subprocess stops the GUI's own server when a restart is started from a
# button. It imports NiceGUI only when an event loop is running -- that is, only when the
# GUI is up -- so the core still runs without it.
ignore_imports = [
"maptasker.src.core.maputils -> nicegui",
]