edit-cfg-json-tk
There are 3 related packages for editing a config-as-json
configuration:
-
edit-cfg-json-tk a desktop editor based on Tkinter. It is a thin backend on top of the core.
-
edit-cfg-json-textual a terminal editor based on Textual. It is a thin backend on top of the core.
-
edit-cfg-json the user interface agnostic core. It discovers the editable structure of a
config_as_json.Configobject by introspection, and owns all editing, validation and file handling. It is also the package a third party writes a new user interface backend against. The only backend it ships itself is a very limited non-interactive one that prints the model once and returns, for a script, a test or a continuous integration job.
The application supplies its own Config object and gets a folding
editor for it, without writing any user interface code and without
describing its configuration schema a second time.
The three packages share a version number and are released together. The first two are the editors: pick the one that matches how your application is used, and it pulls in the core itself.
Project status
Alpha. No API stability and no backward compatibility is offered while this package is in Alpha. That applies to the core and to both backends. Public names may change without a major version bump.
Semantic versioning starts when the Alpha period ends. Until then, pin an exact version if your build needs to be reproducible.
What this package does
edit-cfg-json-tk is the Tkinter desktop editor. It is a thin backend on
top of edit-cfg-json, which it installs as a dependency. All the
editing, validation and file handling logic lives in the core; this
package only draws it and forwards the user's actions.
Tkinter itself is not installable from PyPI. It comes with most Python
distributions, but on some Linux distributions it is a separate system
package, such as python3-tk.
Main entry points
Everything a user of this package needs is re-exported from the top-level
edit_cfg_json_tk package, so it can be imported directly:
from edit_cfg_json_tk import TkEditor, edit
edit is the short way in for an application that has already chosen
Tkinter. It is edit_cfg_json.edit with this package's backend filled in,
and it gives back the configuration object that was saved, or None when
nothing was:
from edit_cfg_json_tk import edit
saved = edit(config=config, in_file='my_config.json')
TkEditor is the Tkinter implementation of the EditorBackend protocol of
edit-cfg-json, for an application that builds the model itself. It has the
one method that protocol asks for:
from edit_cfg_json import EditModel, load_config
from edit_cfg_json_tk import TkEditor
loaded = load_config(config=config, in_file='my_config.json')
model = EditModel(config=loaded.config, report=loaded.report,
out_file='my_config.json')
TkEditor().run_editor(model)
saved = model.saved_config
The edit-cfg-json-tk program
Installing this package also installs a program of the same name, so an application author gets a Tk editor for their own configuration class without writing a line of code:
edit-cfg-json-tk --module myapp.config --class AppConfig -i /etc/myapp.json
The window it opens is the one this page describes, on the class that was
named. It is edit_cfg_json.run_cli with this package's backend filled in, so
the command line below is the same one that edit-cfg-json-textual has; what
differs is which of the two shows the configuration.
Telling it which class to edit
The class is told and never guessed. --module names a module that is
importable, --file names a Python file that is not, exactly one of the two is
required, and --class names the class in it:
edit-cfg-json-tk --module myapp.config --class AppConfig -i /etc/myapp.json
edit-cfg-json-tk --file ./somewhere/cfg.py --class AppConfig
--module uses the ordinary import path, so PYTHONPATH reaches a package
that is not installed. --file puts the folder of the file at the front of the
path and imports the file by its own name, so a file that imports its
neighbours works — but a file that belongs to a package and uses a relative
import cannot be loaded from a bare path at all, and is refused with a message
saying to use --module with PYTHONPATH instead.
Importing a module runs it. That is the same exposure as running the file
with python, and it is not guarded against, because a configuration class is
Python and reaching it means importing the module it is in.
The rest of the command line
| Option | Meaning |
|---|---|
-i, --input |
Configuration file to read. Without it the editor starts from the values the class declares. |
-o, --output |
Configuration file to write. Without it the input file is written, which is what an editor is normally asked to do. |
--policy |
What to do about a declared value the file does not hold: strict-then-defaults, which is the default, strict or defaults. |
--descriptions |
Name of an edit_cfg_json.Descriptions mapping beside the class, saying what its members are for. Without it the members are shown with whatever their own types say about them, which for most of them is nothing. |
A member has no docstring at runtime, so what a member is for is either in a
mapping like that or nowhere at all, which is why --descriptions exists: it is
the one thing an application knows that this program could not otherwise pass
on. The docstring of the configuration class needs no option, because the class
carries it.
An application that has more to say about its own configuration — the file name
extension it uses, the key combinations its own user interface has taken, and
what becomes of a file that a save writes over — says it in
edit_cfg_json.Settings, and gets there through edit rather than through this
program. Options for those are what a later version of this program adds.
A class this editor cannot construct on its own
Most configuration classes take the keyword arguments that config_as_json
documents and nothing else, and this program constructs them from the signature
it reads. A class that needs an argument of the application's own — a folder, a
connection, the list of names its own validators accept — is reached through
--loader NAME instead, which names an edit_cfg_json.ConfigLoader in the same
module or file:
edit-cfg-json-tk --module myapp.config --loader make_config -i /etc/myapp.json
Whatever the loader needs beyond the four keyword arguments of that protocol has
to be bound where the loader is written, for instance with
functools.partial, because a command line cannot supply an argument this
library knows nothing about. edit_cfg_json.derived_loader is one line for the
ordinary case:
make_config = derived_loader(partial(AppConfig, known_teams=TEAMS))
At least one of --class and --loader is needed and both are allowed. A
loader may choose its class by looking at the file it is given, and --class
beside it is then how a script says which class it is prepared to go on with:
the run stops with its own exit code if the loader answers with another one.
How the run ends
The program is meant to be usable from a script, so each way of refusing has an exit code of its own:
| Code | What it means |
|---|---|
0 |
Everything the program was asked to do was done. |
1 |
The input file cannot be opened for editing. |
2 |
The command line itself is wrong. |
3 |
The module that --module names cannot be imported. |
4 |
The file that --file names cannot be read. |
5 |
That file is not Python that can be imported. |
6 |
That file needs the package it belongs to. |
7 |
The module holds no such name. |
8 |
That name is not a class based on config_as_json.Config. |
9 |
The editor cannot construct that class on its own. |
10 |
The values are not ones the application would accept. |
11 |
The output file was asked for and was not written. |
12 |
The values of that class cannot be written as JSON, so there is nothing to show. |
13 |
The name that --loader names cannot be called at all. |
14 |
The loader needs arguments that a command line cannot supply. |
15 |
The loader did not construct the class that --class asked for. |
16 |
The name that --descriptions names is no mapping of any kind. |
The numbers are edit_cfg_json.ExitCode, so a program that runs this one can
name them instead of writing them out.
Codes 10 and 11 are never answered by this program. They belong to a run
whose backend prints once and returns, which is the
python3 -m edit_cfg_json.dump utility of the core package.
A program that gave the user a session ends with success when the user closes
it, whatever is left in the fields, because closing an editor is not a failure.
If the script folder is not on the path
This program is also reachable through the package it belongs to, which needs
nothing to be on PATH:
python3 -m edit_cfg_json_tk --module myapp.config --class AppConfig
Completing the command line
The program completes its own options and file names with argcomplete, which is installed with it. Register it once for your shell:
eval "$(register-python-argcomplete edit-cfg-json-tk)"
What the window shows
The window holds the label of the configuration, what the class says about itself, what reading the input file did, and then one row per node of the configuration. Below those, in a part of the window that does not scroll, are the validation verdict, the saving line, and the buttons: Validate, Save, Save as..., a tick-box for Explain, a button that folds or opens every container, and Close. Every one of them has a key as well.
Every change of a field goes straight into the model, and the label above the rows is marked while the model holds a change worth saving.
A field is shown with a background, a border and a caret colour of its own, so that what can be typed into can be told from what only says something. Those are stated rather than inherited: the window is white, so a field that kept the background it was given could not be seen at all.
One row per node
A member that holds a list, a dict or a nested config_as_json.Config object
is not one field. It is a row of its own with the rows of what it holds
indented below it, a field at every value, and no field on the row of the
container itself — which says how many things it holds, or which class the
object at it is, where a value would be.
A container has a control at the left of its row, - while it is open and +
while it is folded, and pressing it hides or shows everything inside it. The
button below the rows does the same to all of them at once, and its text says
what the next press will do: Fold all while anything is open, Unfold all
once nothing is. A configuration with nothing to fold gets neither the button
nor the column that the controls sit in, so the values keep that width.
A nested configuration object shows its own docstring below its row and its own members as the rows under that, in the order its class declares them. Folding it leaves the first paragraph of that docstring, because an object showing less of itself says less about itself.
What one nested object is on its own
Beside the class on the row of a nested object is what that object is when it is asked about itself: valid on its own or refused on its own. A list or a dict of such objects says what the objects in it amount to — valid inside or refused inside — because its row is the only one that folding leaves on the screen.
Folding a node asks every object at or inside it, and so does opening one, so the badge appears as soon as a container is folded out of the way. A member that one of those objects refused says why below itself, exactly as the verdict of the whole configuration does; what an object refused about no member of itself is said at the object.
The words that qualify the badge are the whole point. A rule of the class above may relate two objects across the boundary between them, and then every object is valid on its own while the configuration cannot be written. The verdict line below the rows is the only thing that answers whether the file can be saved.
Changing how many things a member holds
At the end of the line of a node are the controls for its elements: Add,
Del, Up and Down, and only the ones that node really offers. They sit at
the end rather than in a column of their own, so a node that offers none of them
costs the values no width at all, which is what makes four of them affordable.
Add copies: a list or a dict whose class declares that its elements are
configuration objects gets one object of that class holding the values it
declares, and any other list gets a copy of the element the class declares for
it, or of the first element it holds now. Adding an entry to a dict opens a
small dialog for the key, because nothing but the person configuring the
application knows what a new entry is called; a key the dict already holds is
asked about again rather than allowed to take the place of what is there.
A container that cannot be given an element gets no Add at all, and says why
below itself instead — an ordinary dict member, for instance, because
config_as_json matches such a member against the keys its class declares, so
a dict that gained one would be refused by the configuration class itself. That
line is explanation rather than something to act on, so it is muted and the
Explain tick-box covers it.
Validating, saving and closing
Validate runs the validation of the application's own configuration class
and shows what that class would say about the values that are in the fields.
What it said about one node is shown below that node, and the line
below the rows names the nodes it was about, by the whole path to each of them,
so a configuration too tall for the window does not leave the user hunting for
the field. What the class said that is about no single node — a
whole-configuration rule, a key that does not match — stays in that line,
because there is no field it belongs to. Every refused node is marked at once,
and not only the first one, because the editor walks the validation plan itself
rather than stopping where Config.validate() stops.
A pass is not read only: a validator returns the value that is stored back into the member, so the fields are written back from the model afterwards, and a member that a validator rewrote says so beside its field. A pass can also change how many rows there are — a validator that sorts a list and removes its duplicates removes one — and the window then builds its rows again rather than writing into a widget for a value that is no longer there.
Leaving a field asks a smaller question of that one member: whether what
was typed into it means a value of that member at all. It is the question a
parse_converters() entry answers, an enum being the case that arises in
practice, and it is asked when the field loses the focus rather than on every
key, because a name that is being typed is no name of a member for most of
the time it takes to type it.
Save writes the output file, and refuses to write values the application would not accept: the diagnostics then say what is wrong with them and the file on disk is left exactly as it was. Saving runs the same pass as Validate does, so it can rewrite a value as well, and the fields show what really reached the file. What was written is no longer waiting to be written, so the mark above the rows goes away and the editor stays open.
Save as asks for the file with the ordinary system dialog. What that dialog
offers is what the application decided in its edit_cfg_json.Settings: the
extension it uses for its configuration is the one the dialog adds to a name
that has none, and the one it offers to filter by, and an application that
enforces its extension gets that filter and no other. An application with no
opinion gets a dialog with none, because this library has none of its own
about what a configuration file is called. Save asks the same question when
the session has no file to write yet, which is what every editor does.
A save that would write over a file this session has not written asks first, in a dialog whose default answer is the one that leaves the file alone. The previous content is then kept under the name the application chose, and the saving line says where it went. Both the question and the name are the core's, so this backend and the Textual one cannot treat the user's old configuration differently. The system dialog is told not to ask about overwriting itself, although it offers to: the question is asked once, and it is asked by the editor.
Close writes nothing of its own. It is the "cancel" of the editor, and it is called Close rather than Cancel because saving leaves the editor open: a button called Cancel beside values that have already been written would read as an offer to undo the writing, which it is not.
Closing an editor that holds something unsaved asks whether the changes may be dropped, and the answer that keeps them is the one the dialog opens on. The button, the key and the close button of the window all go through one place, because the one way out that is not a widget of the editor would otherwise be the one way out that drops the changes silently. Closing again after a Save asks nothing, because a save leaves nothing to lose.
Explain shows or hides what the application says about these values: the whole docstring of the configuration class above the rows, the docstring of each nested object, the description of each described member below its own field, what kind of value each member holds, and why a container cannot be given an element. The editor opens with them shown, and what is left when they are hidden is the first paragraph of the class docstring, because one line for the whole configuration is worth keeping. A member the application described gets a line and one it said nothing about gets none, rather than an empty one. Which of the two states the editor is in belongs to the model, so this backend and the Textual one cannot disagree about it.
It is a tick-box rather than a button, and the tick is what says which of the two states the window is in: a button saying Explain beside explanations that are already there would be offering something that has been done. The key of the action moves the tick with it, because Tk moves it only when it was the tick-box that was pressed.
What reading the input file did is shown above the rows, when it did anything, because it is what explains the marks below it: a member that the file did not hold says so beside its field, and so does one whose value the reading of the file put there or altered — with the older key it was read from, where the class recorded one. Both the message and the marks are read from the model, so the two backends cannot tell the user two different things about one file.
Scrolling, and the colours
The label, the docstring, the load message and the member rows are on a canvas that scrolls, and the verdict, the saving line and the buttons are below it and stay where they are: they are what a user reaches for after editing rather than something to scroll to. The scrollbar is beside the canvas, and the mouse wheel scrolls it however the platform reports one.
The window opens at the size the configuration asks for, up to the size of a window, so a small configuration gets a small window and a large one is scrolled through rather than cut off. A long list therefore does not decide the size of the window twice: it opens folded when opening it would add more rows than the editor opens at, and the window is the size of what is on the screen.
Every text that is a paragraph — the docstring, a description, a message, what is wrong with a member — wraps to the width there is, whatever the user resizes the window to. The mark of a member is the one text that does not wrap, because it belongs beside its field on one line: a window too narrow for the name, the field and the mark squeezes the field, which the user can scroll within, rather than cutting off a mark, which they could not read at all.
Each kind of text has a colour, so that the explanations do not read as loudly
as the values and a refused validation does not read like an accepted one.
Which kind each piece of text is comes from edit_cfg_json.Emphasis and is
therefore the same here as in the Textual backend; what the colours are is this
package's own, in EMPHASIS_COLOURS, because Tk has no theme to ask. They are
chosen for the light window that Tk gives this editor. A Tk that a platform has
put into a dark mode would want other values, and that is a theming decision
the library has not been asked for yet.
About the keys
The keys are the ones the application chose in the actions of its
edit_cfg_json.Settings, and with an application that chose nothing they
are the defaults of edit_cfg_json.ActionSettings:
| Key | What it does |
|---|---|
ctrl+r, or f5 |
Validate |
ctrl+s |
Save |
ctrl+shift+s or f12 |
Save as |
f1, or ctrl+g |
Explain |
f2, or ctrl+t |
Fold all, or unfold all |
ctrl+q |
Close |
Combinations are written in the notation that ActionSettings documents,
which this package translates into the event sequences of Tk: ctrl+shift+s
becomes <Control-Shift-S>, and f5 becomes <F5>. A combination this
translation does not know, or one that Tk itself refuses, leaves that action
without that key rather than without an editor — every action here has a
button as well, which is also what an action the application gave no key at
all keeps. The fold action is offered at all only to a configuration that has
something to fold, so its keys are free wherever there would be nothing to
fold.
The cancel action is bound to nothing in this backend. The questions it would
leave are put in the toolkit's own dialogs, which answer that key themselves.
The bindings are made on the window, so a key that a field does not use for itself reaches them wherever the focus is. They are read once, when the widgets are built, which is the one thing a later answer from a settings callable cannot change.
Installing edit-cfg-json-tk
On macOS and Linux
To install edit-cfg-json-tk on macOS and Linux, run the following command:
pip3 install --upgrade edit-cfg-json-tk
On Microsoft Windows
To install edit-cfg-json-tk on Microsoft Windows, run the following command:
pip install --upgrade edit-cfg-json-tk
Documentation
-
Design and decisions: doc/design.md
-
Public API: edit-cfg-json, edit-cfg-json-tk, edit-cfg-json-textual
-
Protected API: edit-cfg-json, edit-cfg-json-tk, edit-cfg-json-textual
-
Worked examples: examples/src/example
License
edit-cfg-json-tk is released under the MIT License. See the LICENSE.txt
file included in the distribution.
Test summary
- Test result: 1465 passed, 3 deselected in 38s
- No flake8 warnings.
- No mypy errors found.
- No pylint warnings.
- No python layout warnings.
- Built version(s): 0.0.2
- Build and test using Python 3.14.6
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file edit_cfg_json_tk-0.0.2.tar.gz.
File metadata
- Download URL: edit_cfg_json_tk-0.0.2.tar.gz
- Upload date:
- Size: 34.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eaebb38ed726e7c00fc4273912c66586595269671f753f90ef423e79a4b9f68a
|
|
| MD5 |
d1cf6076d721565282d779ebc425fccb
|
|
| BLAKE2b-256 |
cd91529d35fb970d179a0baca8e0280d8539d8f030da2337d7d18b44ee5143fa
|
File details
Details for the file edit_cfg_json_tk-0.0.2-py3-none-any.whl.
File metadata
- Download URL: edit_cfg_json_tk-0.0.2-py3-none-any.whl
- Upload date:
- Size: 37.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3da711a8de161e34c22f29be29022a6831717bf2fa0b7491ee96424520d80644
|
|
| MD5 |
c18995ca2435e38e5c043eaaf74d80b8
|
|
| BLAKE2b-256 |
77fc5d8b36c5286f0feeb9b741f24cab7002eaf9bd8df14c337c0b16da7b8439
|