From 9e05bf26d81212dd0e174d45d4b5c71f19787377 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Fri, 24 Jul 2026 03:58:54 -0700 Subject: [PATCH 1/2] docs: update style guide --- docs/contributing.md | 22 +--- docs/developing.md | 2 +- docs/style.md | 264 +++++++++++++++++++++++++++++-------------- 3 files changed, 184 insertions(+), 104 deletions(-) diff --git a/docs/contributing.md b/docs/contributing.md index ad91fe250..327c55331 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -117,7 +117,7 @@ Licensing is now accomplished using the [REUSE](https://reuse.software/spec-3.3/ !!! tip "PR Scope" If you're unsure where to stop the scope of your PR, ask yourself: _"If I broke this up, could any parts of it still be used by the project in the meantime?"_ -### Workflow Checks +### :material-check-circle: Workflow Checks When pushing your code, several automated [workflows](https://github.com/TagStudioDev/TagStudio/tree/main/.github/workflows) will check it against predefined tests and style checks. It's _highly recommended_ that you run these checks locally beforehand to avoid having to fight back-and-forth with the workflow checks inside your pull requests. These checks currently include: @@ -126,7 +126,7 @@ When pushing your code, several automated [workflows](https://github.com/TagStud - [Pytest](developing.md#pytest) tests - REUSE [license compliance](#licenses) -### Runtime Requirements +## :material-timer-play: Runtime Requirements Code must function on all of the supported operating systems and versions: @@ -134,18 +134,8 @@ Code must function on all of the supported operating systems and versions: - macOS 14.0+ - Common Linux distributions and versions -## :material-file-document: Documentation Guidelines +Final submitted code must **_NOT:_** -Documentation contributions include anything inside of the `docs/` folder as well as the `README.md`. Documentation inside the `docs/` folder is built and hosted on our static documentation site, [docs.tagstud.io](https://docs.tagstud.io/). - -- Use "[dash-case / kebab-case](https://developer.mozilla.org/en-US/docs/Glossary/Kebab_case)" for file and folder names -- Follow the folder structure pattern -- Don't add images or other media with excessively large file sizes -- Provide alt text for embedded media -- Use "[Title Case](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case)" for title capitalization - -## :material-translate: Translation Guidelines - -Translations are performed on the TagStudio [Weblate project](https://hosted.weblate.org/projects/tagstudio/). - -_Translation guidelines coming soon._ +- Contain superfluous or unnecessary logging statements +- Cause unreasonable slowdowns to the program outside of a progress-indicated task +- Cause undesirable visual glitches or artifacts on screen diff --git a/docs/developing.md b/docs/developing.md index 4b5252d40..9dba3bdec 100644 --- a/docs/developing.md +++ b/docs/developing.md @@ -214,7 +214,7 @@ From there, Git will automatically run through the hooks during commit actions! You can automatically enter this development shell, and keep your user shell, with a tool like [direnv](https://direnv.net/). Some reference `.envrc` files are provided in the repository at [`contrib`](https://github.com/TagStudioDev/TagStudio/tree/main/contrib). -Two currently available are for [Nix](#nixos) and [uv](#installing-with-uv), to use one: +Two currently available are for [Nix](#nix-nixos) and [uv](#installing-with-uv), to use one: ```sh ln -s .envrc-$variant .envrc diff --git a/docs/style.md b/docs/style.md index ac5a7479d..336ad05fd 100644 --- a/docs/style.md +++ b/docs/style.md @@ -6,116 +6,206 @@ icon: material/sign-text + +!!! abstract "Prerequisite Reading" + This guide assumes you've read the [Developing](developing.md) and [Contributing](contributing.md) pages first. + # :material-sign-text: Style Guide -## Formatting - -Most of the style guidelines can be checked, fixed, and enforced via Ruff. Older code may not be adhering to all of these guidelines, in which case _"do as I say, not as I do"..._ - -- Do your best to write clear, concise, and modular code. - - This should include making methods private by default (e.g. `__method()`) - - Methods should only be protected (e.g. `_method()`) or public (e.g. `method()`) when needed and warranted -- Keep a maximum column width of no more than **100** characters. -- Code comments should be used to help describe sections of code that can't speak for themselves. -- Use [Google style](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) docstrings for any classes and functions you add. - - If you're modifying an existing function that does _not_ have docstrings, you don't _have_ to add docstrings to it... but it would be pretty cool if you did ;) -- Imports should be ordered alphabetically. -- Lists of values should be ordered using their [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order). - - Some files have their methods ordered alphabetically as well (i.e. [`thumb_renderer`](https://github.com/TagStudioDev/TagStudio/blob/main/src/tagstudio/qt/widgets/thumb_renderer.py)). If you're working in a file and notice this, please try and keep to the pattern. -- When writing text for window titles or form titles, use "[Title Case](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case)" capitalization. Your IDE may have a command to format this for you automatically, although some may incorrectly capitalize short prepositions. In a pinch you can use a website such as [capitalizemytitle.com](https://capitalizemytitle.com/) to check. -- If it wasn't mentioned above, then stick to [**PEP-8**](https://peps.python.org/pep-0008/)! - -### Modules & Implementations - -- **Do not** modify legacy library code in the `src/core/library/json/` directory -- Avoid direct calls to `os` - - Use `Pathlib` library instead of `os.path` - - Use `platform.system()` instead of `os.name` and `sys.platform` -- Don't prepend local imports with `tagstudio`, stick to `src` -- Use the `logger` system instead of `print` statements -- Avoid nested f-strings -- Use HTML-like tags inside Qt widgets over stylesheets where possible +## :material-script-text: General Principles -Final submitted code must **_NOT:_** +- Write **clear**, **concise**, and **modular** code. +- If the purpose of a peice of code is not obvious, a **short comment** should help explain it. +- Remember to follow the rest of the [contribution guidelines](contributing.md)! -- Contain superfluous or unnecessary logging statements -- Cause unreasonable slowdowns to the program outside of a progress-indicated task -- Cause undesirable visual glitches or artifacts on screen +--- -### Formatter Configs +## :material-text-box-check: Formatting + +Linting in Python files is mostly taken care of by [ruff](developing.md#ruff) and is based on the rules declared in `pyproject.toml` and `.editorconfig`. TagStudio provides an [EditorConfig](https://editorconfig.org/#example-file) file ([`.editorconfig`](https://github.com/TagStudioDev/TagStudio/blob/main/.editorconfig)) along with a [Prettier](https://prettier.io/) config file ([`.prettierrc.toml`](https://github.com/TagStudioDev/TagStudio/blob/main/.prettierrc.toml)) for formatting files other than .py files (Markdown, JSON, YAML, HTML, CSS, etc.). If editing these types of files it's recommended that you use a formatter that supports EditorConfig or has its settings matched to the EditorConfig and Prettier configs. Lastly, please pay attention to the `prettier-ignore` flags in present in some files if you are not using Prettier, as formatting these sections will break formatting used elsewhere such as the [MkDocs site](https://docs.tagstud.io/). -## Qt +### :material-code-braces-box: Syntax Guidelines -As of writing this section, the QT part of the code base is quite unstructured and the View and Controller parts are completely intermixed[^1]. This makes maintenance, fixes and general understanding of the code base quite challenging, because the interesting parts you are looking for are entangled in a bunch of repetitive UI setup code. To address this we are aiming to more strictly separate the view and controller aspects of the QT frontend. +- Python files should always follow the [**PEP 8**]() style guide conventions, unless specifically allowed otherwise. + - The most notable exception in our project is the line length limit of **100** characters, which is enforced via ruff. + - Internal Qt methods also use `camelCase` instead of `snake_case`, so overrides of those are commonly seen in the codebase. +- Classes and attributes considered to be "[private](https://docs.python.org/3/tutorial/classes.html#private-variables)" should be prepended with a **single underscore** (e.g. `_internal_method()`). + - If _functionally necessary_, an attribute name may be prepended with a double underscore to trigger "[name mangling](https://docs.python.org/3/reference/expressions.html#private-name-mangling)" (e.g. `__mangled_method()`). +- Classes and methods should contain [Google style](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) docstrings _(this style is enforced via ruff)_. +- Lists and JSON keys should be ordered by their [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order) unless otherwise specified or readily indicated. +- Some files have some or all of their attributes sorted. Please respect any established patterns like these in files you modify. -The general structure of the QT code base should look like this: +--- -``` -qt -├── controllers -│ ├── widgets -│ │ └── preview_panel_controller.py -│ └── main_window_controller.py -├── views -│ ├── widgets -│ │ └── preview_panel_view.py -│ └── main_window_view.py -├── ts_qt.py -└── mixed.py -``` +## :material-filter-cog: Modules & Systems + +### :fontawesome-brands-python: Python Modules -In this structure there are the `views` and `controllers` sub-directories. They have the exact same structure and for every `_view.py` there is a `_controller.py` at the same location in the other subdirectory and vice versa. +- Use `Pathlib` library instead of `os.path` +- Use `platform.system()` instead of `os.name` or `sys.platform` +- Avoid nested f-strings -Typically the classes should look like this: +### :material-tag: TagStudio Systems -```py -# my_cool_widget_view.py -class MyCoolWidgetView(QWidget): - def __init__(self): - super().__init__() - self.__button = QPushButton() - self.__color_dropdown = QComboBox() - # ... - self.__connect_callbacks() +- Translation keys can be accessed via bracket notation (e.g. `Translations["translation_key"]`) or with the `Translations.format()` method when a value needs to be passed to a placeholder in the translation. +- Use HTML-like tags inside strings over explicit stylesheets where possible. The `Style` class provides several handy methods for formatting text with these. +- Use the `format` method in the stylesheets class to format text headers. - def __connect_callbacks(self): - self.__button.clicked.connect(self._button_click_callback) - self.__color_dropdown.currentIndexChanged.connect( - lambda idx: self._color_dropdown_callback(self.__color_dropdown.itemData(idx)) - ) +--- - def _button_click_callback(self): - raise NotImplementedError() +## :material-folder-file: Project Layout + +### :material-engine: Core Backend + +Code that is integral to the core functionality of TagStudio and is UI-independent belongs under the `core/` directory. It's possible that some code that serves the UI can go here, as long as its purpose is to serve _any_ UI and is independent from Qt (e.g. file preview rendering). + +```yaml title="Core Backend Directory Example" +core/ +│ +├── library/ # The TagStudio library system +│ │ +│ ├── alchemy/ # Current SQLite backend w/ SQLAlchemy ORM +│ │ +│ ├── json/ # Read-only legacy JSON library system, kept for migrations +│ │ +│ ├── query_lang/ # The query parser +│ │ +│ │ # Library files that do not involve the SQLAlchemy ORM +│ │ # NOTE: Future Non-SQLAlchemy library files will be placed here +│ ├── refresh.py +│ └── ... +│ +└── utils/ # Utility classes and functions for the core ``` -```py -# my_cool_widget_controller.py -class MyCoolWidget(MyCoolWidgetView): - def __init__(self): - super().__init__() + +!!! danger "Read-Only Legacy Code" + **Do not modify** legacy library code in the `src/core/library/json/` directory! - def _button_click_callback(self): - print("Button was clicked!") +--- - def _color_dropdown_callback(self, color: Color): - print(f"The selected color is now: {color}") +### :material-button-cursor: App UI Frontend + +The application UI code is stored in the `qt/` directory, and contains all code specific to the Qt frontend. Qt widgets are built using an [MVC](https://www.geeksforgeeks.org/software-engineering/mvc-framework-introduction/) pattern, which is described in-depth below: + +#### MVC Pattern + +- **Models** are usually just objects from the [library](#core-backend). + - The **controller** interacts with these, the _view_ does **not**. +- **Views** are Qt layout classes that **_only_** contain the **layout** and **styling** for one or more widgets. + - Class names are appended with `View`, and filenames appended with `_view`. + - Not to be used standalone, but as the layouts for one or more controllers. + - Some logic is acceptable in these classes if it serves to modularize the layout and allows controllers to influence how the layout is initialized. + - **Reusable Layouts** + - If a layout class is **_not meant_** to act as a view but instead be a generic layout, it belongs in the `views/layouts/` directory and the file should be appended with `_layout`. + - **Styling Classes** + - If a class is purely a source of reusable styling, it belongs in the `views/styles/` directory. + +- **Controllers** are complete widgets or base classes for complete widgets. + - Controller files simply take on the name of the final widget they create. + - This also creates naming parity with other widgets that simply extend existing Qt widgets with additional logic. + +```yaml title="Qt Frontend Directory Example" +qt/ +│ +├── controllers/ # Widgets implementing views or extending other widgets +│ ├── tag_suggest_box.py # Extends from `suggest_box.py` +│ ├── suggest_box.py # Implements `suggest_box_view.py` +│ ├── main_window.py +│ └── ... +│ +├── mixed/ # Files yet to be refactored into controllers and views +│ +├── views/ # Everything related to widget layouts and appearances +│ │ +│ ├── layouts/ # Layouts meant to be reused on their own inside other layouts +│ │ ├── flow_layout.py +│ │ └── ... +│ │ +│ ├── styles/ # Classes specific for styling +│ │ ├── palette.py +│ │ ├── stylesheets.py +│ │ └── ... +│ │ +│ │ # Views (layouts) that get implemented by controllers (widgets) +│ ├── main_window_view.py +│ ├── suggest_box_view.py +│ └── ... +│ +│ # Frontend classes that aren't related to widgets, like managers +├── resource_manager.py +├── cache_manager.py +├── ts_qt.py # Qt Driver +└── ... ``` -Observe the following key aspects of this example: + +!!! warning "Pre-MVC UI Code" + **Do not add** new files to the `qt/mixed/` directory! These files have yet to to be refactored per the current MVC style guidelines and the directory will be **removed** once those migrations have concluded. + +Observe the following key aspects of the example below: -- The Controller is just called `MyCoolWidget` instead of `MyCoolWidgetController` as it will be directly used by other code -- The UI elements are in private variables - - This enforces that the controller shouldn't directly access UI elements - - Instead the view should provide a protected API (e.g. `_get_color()`) for things like setting/getting the value of a dropdown, etc. - - Instead of `_get_color()` there could also be a `_color` method marked with `@property` -- The callback methods are already defined as protected methods with NotImplementedErrors - - Defines the interface the callbacks - - Enforces that UI events be handled +- The **view** extends from a Qt layout class, and the **controller** simply extends from QWidget. +- The **controller** is just called `MyCoolWidget` instead of `MyCoolWidgetController` as it will be directly used by other code. +- The **view's** widgets that are intended to be controlled by the controller are **public**, while the **controller's** methods are largely **private**. + + +!!! example "MVC-Separated Widget Example" + + ```py title="views/my_cool_widget_view.py" + class MyCoolWidgetView(QVBoxLayout): + def __init__(self): + super().__init__() + self.button = QPushButton() + self.color_dropdown = QComboBox() + + self.addWidget(self.button) + self.addWidget(self.color_dropdown) + ``` + + ```py title="controllers/my_cool_widget.py" + class MyCoolWidget(QWidget): + def __init__(self): + super().__init__() + self.setLayout(MyCoolWidgetView()) + self._connect_callbacks() + + def _connect_callbacks(self): + self.layout().button.clicked.connect(self._button_click_callback) + self.layout().color_dropdown.currentIndexChanged.connect( + lambda idx: self._color_dropdown_callback(self.color_dropdown.itemData(idx))) + + def _button_click_callback(self): + print("Button was clicked!") + + def _color_dropdown_callback(self, color: Color): + print(f"The selected color is now: {color}") + ``` -!!! tip - A good (non-exhaustive) rule of thumb is: If it requires a non-UI import, then it doesn't belong in the `*_view.py` file. +!!! tip "Tip for Logic Placement" + A good rule of thumb is: If there's **conditional logic** after a widget has been created, it should probably go in a **controller**. + +--- + +## :material-file-document: Documentation + +Documentation contributions include anything inside the `docs/` folder as well as the `README.md`. Documentation inside the `docs/` folder is built and hosted on our static documentation site, [docs.tagstud.io](https://docs.tagstud.io/). Some files such as the `CHANGELOG.md`, `CONTRIBUTING.md`, and `STYLE.md` are symlinked in the repo root from the `docs/` folder. + +- Use "[dash-case / kebab-case](https://developer.mozilla.org/en-US/docs/Glossary/Kebab_case)" for file and folder names +- Follow the folder structure pattern +- Don't add images or other media with excessively large file sizes +- Provide alt text for embedded media +- Use "[Title Case](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case)" for title capitalization + +--- + +## :material-translate: Translations + +Translations are performed on the TagStudio [Weblate project](https://hosted.weblate.org/projects/tagstudio/). -[^1]: For an explanation of the Model-View-Controller (MVC) Model, checkout this article: [MVC Framework Introduction](https://www.geeksforgeeks.org/mvc-framework-introduction/). +- Do not change text inside placeholders +- Do not change the style tags inside translations +- Use the glossary for term definitions From 43adc77b95c5aa650dbc9941d2cbc66fbc9dcc78 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:51:57 -0700 Subject: [PATCH 2/2] docs: add driver argument guideline --- docs/style.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/style.md b/docs/style.md index 336ad05fd..5f8b90019 100644 --- a/docs/style.md +++ b/docs/style.md @@ -50,6 +50,7 @@ TagStudio provides an [EditorConfig](https://editorconfig.org/#example-file) fil ### :material-tag: TagStudio Systems - Translation keys can be accessed via bracket notation (e.g. `Translations["translation_key"]`) or with the `Translations.format()` method when a value needs to be passed to a placeholder in the translation. +- Avoid passing around the `QtDriver` class where possible. Instead, pass only the necessary components such as the `Library` and `GlobalSettings` instances. - Use HTML-like tags inside strings over explicit stylesheets where possible. The `Style` class provides several handy methods for formatting text with these. - Use the `format` method in the stylesheets class to format text headers.