Skip to main content

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.Config object 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

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

edit_cfg_json_tk-0.0.2.tar.gz (34.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

edit_cfg_json_tk-0.0.2-py3-none-any.whl (37.3 kB view details)

Uploaded Python 3

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

Hashes for edit_cfg_json_tk-0.0.2.tar.gz
Algorithm Hash digest
SHA256 eaebb38ed726e7c00fc4273912c66586595269671f753f90ef423e79a4b9f68a
MD5 d1cf6076d721565282d779ebc425fccb
BLAKE2b-256 cd91529d35fb970d179a0baca8e0280d8539d8f030da2337d7d18b44ee5143fa

See more details on using hashes here.

File details

Details for the file edit_cfg_json_tk-0.0.2-py3-none-any.whl.

File metadata

File hashes

Hashes for edit_cfg_json_tk-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 3da711a8de161e34c22f29be29022a6831717bf2fa0b7491ee96424520d80644
MD5 c18995ca2435e38e5c043eaaf74d80b8
BLAKE2b-256 77fc5d8b36c5286f0feeb9b741f24cab7002eaf9bd8df14c337c0b16da7b8439

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page