Editable and pinned features

Every feature a product declares is either editable or pinned. An editable feature is a working copy you change. A pinned feature is a released version you only use. What tells them apart is a version suffix in the product’s pyproject.toml.

Table of contents

The difference in one paragraph

An editable feature is declared by name alone, as in splent_io/splent_feature_events. The product links it to a working copy at the workspace root, <workspace>/splent_feature_events/, and whatever you change there is what the product runs. A pinned feature is declared with a version, as in splent_io/splent_feature_auth@v1.17.0. The product links it to a read-only snapshot of that git tag under .splent_cache/, so it runs exactly that release on every machine. You develop with editable features and you ship pinned ones, and the CLI moves a feature from one mode to the other without you copying any code.

Pinned features are also called versioned features, and the cache calls their directories versioned snapshots. All three names mean the same thing.


Side by side

  Editable Pinned
Entry in pyproject.toml The name alone The name plus a version, @v1.17.0
Code lives in A working copy at the workspace root A snapshot under .splent_cache/features/
Files Read-write Read-only, a shallow clone of the tag
Copies in a workspace One working copy, shared by every product that declares the feature without a version One directory per version, so several versions sit side by side
Your edits Reach the development app without reinstalling Not allowed. Unlock the feature first
db:migrate Generates migrations Skips the feature and tells you to unlock it
Mode in splent.manifest.json editable pinned
Production image installs Whatever PyPI serves under the package name, never your working copy That exact release from PyPI, or its git tag when PyPI does not serve it
product:build and product:release Refused (see Environment rules) Required
Brought into a product with feature:add feature:attach
Good for Changing the feature Using the feature, and anything that goes to production

Declaring each kind

A product declares its features in three lists under [tool.splent], described in Feature groups. Each list accepts both kinds, mixed in any order.

[tool.splent]
features = [
    "splent_io/splent_feature_auth@v1.17.0",    # pinned
    "splent_io/splent_feature_theme@v0.12.1",   # pinned
    "splent_io/splent_feature_events",          # editable
]
features_dev = [
    "splent_io/splent_feature_mailhog@v1.0.7",  # pinned, development only
]
features_prod = []

A few rules govern these entries.

  • The part after @ is the git tag of a release, v included. The production image asks PyPI for the same version without the v, so @v1.17.0 becomes splent_feature_auth==1.17.0.
  • An entry without @ is editable. Nothing else in the product decides the mode, and the mode that splent.manifest.json records is derived from the entry.
  • The namespace can be written splent_io or splent-io. Both name the same namespace, and the commands that write entries keep whichever spelling the product already uses.
  • A feature is declared once. feature:add and feature:attach replace any earlier declaration of the same feature, in any list and in either mode, together with the symlink it created.

The commands below keep these lines up to date, so you rarely write them by hand. If you do edit the file, run product:resolve afterwards so the symlinks follow.


Where each lives on disk

A product never holds feature code itself. Its features/<namespace>/ directory holds relative symlinks, one per declared feature, and the name of each symlink repeats the entry, version included when there is one.

splent_workspace/
├── splent_framework/
├── splent_cli/
├── splent_feature_events/            ← working copy, read-write
├── .splent_cache/
│   └── features/
│       └── splent_io/
│           ├── splent_feature_auth@v1.16.0/   ← snapshot, read-only
│           └── splent_feature_auth@v1.17.0/   ← snapshot, read-only
└── my_first_app/
    ├── pyproject.toml
    └── features/
        └── splent_io/
            ├── splent_feature_auth@v1.17.0    → the v1.17.0 snapshot
            └── splent_feature_events          → the working copy

Editable features sit at the workspace root, next to splent_framework/ and splent_cli/, in a directory named after the package. Pinned features sit in the cache, one directory per version, named <feature>@<version>. When the application starts, the framework turns each entry back into the symlink path, features/<namespace>/<name>@<version> or features/<namespace>/<name>, to find the feature (see Feature loading).

Because the working copy is shared, two products in the same workspace that both declare splent_io/splent_feature_events without a version run the same code, and a change made for one is seen by the other. A product that must stay on a known release pins the feature, and keeps running its snapshot whatever happens in the working copy. The SPLENT Cache page covers the cache itself and the commands that inspect and prune it.


What happens to your edits

Editable features

The symlink makes the working copy the product’s copy. The development container installs the feature with pip install -e from that path, so a change to its code or templates reaches the development server without reinstalling the package, and there is nothing to derive again. You commit and push from the working copy as from any git repository.

Pinned features

Snapshots are not meant to be edited. When the CLI clones a version into the cache it makes every file read-only (git’s own files under .git excepted), and commands that would change a feature’s code keep away from them.

  • db:migrate skips a pinned feature and tells you to run feature:unlock first. A release ships its migrations, and only an editable feature gets new ones.
  • lint only checks the features at the workspace root.

A change forced into a snapshot would not travel anyway. product:resolve --force clones the snapshot again, and a production image never reads the cache, because it installs the release from PyPI or from its git tag. To change a pinned feature, unlock it.


Moving between the two

 pinned                                   editable
 splent_feature_auth@v1.17.0              splent_feature_auth
 in .splent_cache/, read-only             at the workspace root, read-write

          ─────────── feature:unlock ───────────▶

          ◀────────── feature:release, then attach
                      feature:attach <version>
                      feature:pin
                      feature:upgrade
Command From To What it does
feature:add Not declared, or pinned Editable Declares <namespace>/<name>, links the working copy at the workspace root and reinstalls the feature in the running web container. The working copy must already exist. A pinned declaration of the same feature is replaced, so this is also the quick way back to a working copy you already have.
feature:attach Not declared, editable, or another version Pinned Declares <namespace>/<name>@<version>, fetching that version into the cache first when it is not there, links the snapshot and reinstalls. An editable declaration is replaced, and the working copy stays where it is.
feature:unlock Pinned Editable Copies the snapshot to the workspace root when no working copy exists there yet, makes it writable, points origin at the forge, switches to main and pulls. Then it drops the @version from the entry, swaps the symlink and reinstalls. The snapshot stays in the cache.
feature:release Editable Pinned, once attached Cuts a new version from the working copy (tag on the forge, package on PyPI), clones the tag into the cache as a read-only snapshot, then attaches it. It asks first, or attaches straight away with --attach. It refuses a reference that already carries a version.
feature:pin Editable Pinned Rewrites every editable entry, in all three lists, to the highest version already in the cache. It does not ask the forge, and it leaves the symlinks to product:resolve.
feature:upgrade Pinned or editable Pinned Moves an entry to the latest tag on the forge, cloning it when needed and replacing the symlink. An editable entry is offered that tag too.
feature:outdated Pinned Pinned Compares each pinned feature with the latest tag on the forge and reports which ones are behind. With --upgrade it runs feature:upgrade for each of those.
feature:detach Pinned Editable entry, no symlink Removes the snapshot’s symlink and uninstalls the feature, but only drops the @version from the entry, which stays in pyproject.toml as an editable declaration.
feature:remove Either Not declared Removes the entry whatever its version, its symlink and its manifest record, and uninstalls the feature. The working copy and the cache are left untouched.

To take a pinned feature out of a product altogether, use feature:remove. feature:detach leaves a bare editable entry behind, which points at a working copy the product may not have.

Two more commands act on the working copies themselves rather than on the product. feature:pull runs git pull in every editable feature at the workspace root, and feature:discard deletes one, after warning about the products that declare it without a version. Neither touches a snapshot.

feature:install wraps the first steps into one. With --editable it clones the working copy if needed and runs feature:add, and with --pinned it clones the version into the cache and runs feature:attach. Both also merge the feature’s environment variables and start its Docker services, when it has any.

A typical round trip

The product pins splent_io/splent_feature_auth@v1.17.0 and auth needs a fix.

# Turn the pinned feature into a working copy at the workspace root
splent feature:unlock splent_feature_auth

# Edit, test, commit and push in <workspace>/splent_feature_auth

# Cut v1.17.1 from the working copy and pin the product to it
splent feature:release splent_feature_auth --bump patch --attach

The product now declares splent_io/splent_feature_auth@v1.17.1. The working copy stays at the workspace root, so the next feature:unlock reuses it and pulls main instead of copying a snapshot. Keep in mind that main can be ahead of the version you had pinned.


How each is installed

  Development container Production image
What installs features scripts/00_install_features.sh when the container starts, and feature:add, feature:attach and feature:unlock when the web container is already running splent feature:pip-install, in the builder stage of docker/Dockerfile.<product>.prod
Editable entry pip install -e from the working copy, through the product symlink pip install splent_feature_events, whatever PyPI serves under that name
Pinned entry pip install -e from the snapshot, through the product symlink pip install splent_feature_auth==1.17.0, falling back to the v1.17.0 git tag when PyPI does not serve it

pip’s -e and SPLENT’s editable are different ideas. In development both kinds are installed with pip install -e, and the only difference is the directory behind the symlink. In production nothing is installed from the workspace at all, so an editable entry gets whatever PyPI has under the package name, which may be an old release or nothing.


Environment rules

Development accepts both kinds, mixed freely. Production accepts only pinned features, because the image installs from PyPI or from a git tag and has no way to reach a working copy.

Command Editable entries Pinned entries
product:resolve Links the working copy. When there is none at the workspace root it reports the feature as not found and moves on, it never clones one Clones the tag into the cache when it is missing, then links the snapshot
product:derive --dev Accepted. After the pre-flight, the pipeline runs product:resolve first Accepted
product:build Fails the pre-flight for any entry in features or features_prod Each version must be on PyPI or tagged on the forge
product:derive --prod Skips that pre-flight phase, so product:build warns instead and asks whether to continue, defaulting to no Accepted
product:deploy No check of its own. It deploys what product:build produced Same
product:release Refuses to start while any entry in features has no version Required

features_dev is never read by the production commands, so an editable feature declared only there does not block a build or a release.


Which one should I use

Editable, while you are working on the feature itself.

  • You are writing a new feature. feature:create puts it at the workspace root, editable from the start.
  • You need to fix or extend an existing feature, or generate a migration for it.
  • You want a change to show up in every product of the workspace that uses the feature.

Pinned, whenever you only use the feature.

  • The product consumes the feature as it was released.
  • The product is built, deployed or released for production. It has to be.
  • A product must not pick up work in progress from a shared working copy.
  • Every machine and every teammate should run the same code.

A product in day to day development is often mixed, with most features pinned and the one or two you are changing editable. Before it goes to production, release each editable feature you changed and attach the new version, or run feature:pin when the versions you want are already in the cache. The pre-flight of product:build names any feature still left without a version.


See also


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es