Skip to main content

cosmofy 0.2.1 #

Gotta patch 'em all!

cosmofy 0.2.1 is available. Lots of little bugs after this morning's big release.

If you have uv installed you can use cosmofy:

uvx cosmofy

# or install it
uv tool install cosmofy

Read more.

Release Notes 0.2.1 - 2025-12-24T18:35:27Z #

Fixed

  • #42 minor typos; added links in README
  • #58 only do the Windows dance if literally trying to replace sys.executable
  • #96 files not saving to cache
  • #97, #98 executable permissions for Cosmopolitan Python
  • #99 nothing being logged in binary build
  • #95 don't search for pyproject.toml when you only have --script to bundle
  • #104 error when using cosmofy fs ls with --sort none
  • #105 restore working directory after error when using cosmofy fs add with --chdir
  • #103 use context manager for urlopen even for HEAD

Changed

  • #106 cosmofy fs cat outputs raw bytes to sys.stdout instead of decoding to UTF-8

Added

  • #94 pure Python limitation to README
  • #107 clearer docstring for expand_glob; literal paths are returned exactly
  • #100 document that pythonoid.run_python intentionally destroys sys.argv

Security

  • #102 call validate_url at the start of all the download_* functions
  • #109 reject paths with null bytes
  • #108 don't follow circular symlinks endlessly

cosmofy 0.2.0 #

Now using uv!

cosmofy 0.2.0 is available. So many things came together for this release:

  1. Three open source developers I follow (William McGuan, Simon Willison, and Charlie Marsh) were all in a twitter thread where the concept of something like cosmofy was mentioned.
  2. I learned how to make CLIs sort of work the way Astral's tools do.
  3. I learned just how many crazy options GNU ls supports.

If you have uv installed you can use cosmofy:

uvx cosmofy

# or install it
uv tool install cosmofy

Read more.

Release Notes 0.2.0 - 2025-12-24T03:14:02Z #

This release represents a very large shift from bundling individual python files to using uv to bundle entire venv directories. The behavior of the CLI is now much more similar to uv in form and function.

Fixed

  • #14 typo in --help
  • #43 exclude uv artifacts from the bundle
  • #46 expand_globs to follow all (most?) of the shell rules
  • #48 getting only the project's console_scripts
  • #50 cosmofy bundle --script assumes venv in output
  • #52 avoiding clobbering logging on import
  • #56 used context handlers to close temporary directories
  • #58 self update on Windows
  • #59 assumption that Last-Modified will be present in headers
  • #61 replaced PathDistribution._path with appropriate fallbacks
  • #62 replaced assert for validation with actual raise on error
  • #66 needless pass in parsing args
  • #67 command names in logging
  • #70 incorrect shebang in bundle.py
  • #71 used context handlers to close ZipFile objects
  • #72 download progress indicator shouldn't divide by zero
  • #73 missing newline in bundle.py error message
  • #75 explicit return of default when we can't get the version from a file
  • #77 cosmofy fs cat usage string
  • #79 ZipFile2.now should not be at module-level
  • #92 use public API for SourceFileLoader

Changed

  • #16 update examples, readme, usage (HT @Pugio)
  • #28 replaced --debug with --quiet + --verbose
  • #29 DEFAULT_PYTHON_URL is now Cosmopolitan Python
  • #31 replaced --cache with --no-cache + --cache-dir
  • #33 refactored bundler.py into subcommands
  • #55 switched to JSON parsing uv version output
  • #57 switched to using os.replace for atomic file moves
  • #64 replaced removeprefix with better cross-platform approach
  • #79 moved progress indicator to stderr
  • #87 marked cosmofy updater commands as experimental

Added

  • #18 progress when downloading (HT @Pugio)
  • #32 cosmofy bundle --script
  • #33 cosmofy bundle
  • #33 cosmofy fs add
  • #33 cosmofy fs args
  • #33 cosmofy updater remove
  • #33 dry run banner
  • #35 cosmofy fs ls
  • #36 cosmofy fs cat
  • #37 cosmofy fs rm
  • #38 cosmofy fs add -f to overwrite files that exist
  • #39 --color global option
  • #40 cosmofy self update
  • #40 cosmofy self version
  • #49 --exact to uv sync for better exact specs
  • #60 timeouts to network calls
  • #81 note that concurrent removals are not supported
  • #83 missing docstrings
  • #86 log message that only GitHub URLs are inferred
  • #89 warning when adding absolute paths
  • #91 note about handling weird file names

Removed

  • #15 cosmo.yaml workflow; added gh command
  • #17 default .com extension
  • #24 support for python <= 3.9
  • #27 --clone is no longer supported
  • #30 --download is no longer supported

Security

  • #53 added validate_url to avoid non-HTTPS URLs
  • #53 added security considerations note
  • #54 removed shell=True in places that don't need it
  • #63 added sanitize_zip_path to avoid bad entry names

cosmofy fs ls: emulating GNU ls #

How much of GNU ls should I emulate in Python?

Previously: pythonoid, baton

When I was designing the low-level zip file manipulation tools for cosmofy, I wanted an easy way to see the contents of the bundle. We're so used to using ls for looking into directories that I thought it would be cool to emulate as much of ls as I could.

But then it turned out that ls has a crazy number of options. I actually went through them all and tried to figure out if it was possible to support them.

But then I realized this was insane. First, many of the options are just aliases for slightly more explicit options. Charlie Marsh would never have a -t that was an alias for --sort=time. Why should I?

In the end I decided to go with the most common options (sorting, list view), a couple that were easy to implement, and a few longer-form ones that cover most of the aliases.

I'm pretty happy with the way it turned out.


baton: emulating Astral's CLI approach #

How much of the Astral CLI theme can I emulate in Python?

Previously: pythonoid

Imitation is the highest form of flattery which is why as part of the cosmofy 0.2.0 release, I decided to change everything about how the CLI behaved to make it work more like the way the tools from Astral work.

I have a long-term plan for Astral to take over making Cosmopolitan Python apps. It's a long shot, but if they do, it'll be a huge win for cross-platform compatible executables. I also saw this popular issue that there should be a uv bundle command that bundles everything up.

To make it easier to adopt, I decided to make the interface follow Astral's style in three important ways:

  1. Subcommand structure: It's gotta be cosmofy bundle and cosmofy self update
  2. Colored output: Gotta auto-detect that stuff. Luckily, I had fun with brush years ago, so I know about terminal color codes.
  3. Global flags: Some of those flags gotta be global.
  4. Smart ENV defaults: smart defaults + pulling from environment variables to override.

Now I didn't start out wanting to build my own argument parser (really, I promise I didn't!). I tried going the argparse route (I even tried my own attrbox / docopt solution), but I had a few constraints:

  1. I really don't want 3rd party dependencies (even my own). cosmofy needs to stay tight and small.
  2. I want argument parsing to go until it hits the subcommand and then delegate the rest of the args to the subcommand parser.
  3. I want to pass global options from parent to child sub-parser as needed.

Together these pushed for a dedicated parser. This lets me write things like:

usage = f"""\
Print contents of a file within a Cosmopolitan bundle.

Usage: cosmofy fs cat <BUNDLE> <FILE>... [OPTIONS]

Arguments:
{common_args}
  <FILE>...                 one or more file patterns to show

  tip: Use `--` to separate options from filenames that start with `-`
  Example: cosmofy fs cat bundle.zip -- -weird-filename.txt

Options:
  -p, --prompt              prompt for a decryption password

{global_options}
"""


@dataclass
class Args(CommonArgs):
    __doc__ = usage
    file: list[str] = arg(list, positional=True, required=True)
    prompt: bool = arg(False, short="-p")

...

def run(args: Args) -> int:
  ...

cmd = Command("cosmofy.fs.cat", Args, run)
if __name__ == "__main__":
    sys.exit(cmd.main())

For the colored output, I took inspiration from William McGuan's rich which uses tag-like indicators to style text.

Mine is much worse and minimal, but it gets the job done for the bits of color I need.



πŸ“ How I Found Myself Running a Microschool (Kelsey Piper / Center for Educational Progress). Over the past 10 years I have migrated to essentially this view: you need direct instruction to get the basics and a foundation; you need to see people enact the values you want to transmit; and you need a strong motivating project to get you over the humps when the going gets tough.



πŸ“ Ideas Aren’t Getting Harder to Find (Karthik Tadepalli / Asterisk). Knowing what is causing productivity growth to start to slow is critical to selecting appropriate policies for how to get it going again. Karthik makes a good case for why the idea that "ideas are getting harder to find" is wrong and why it's more of a failure of the market to weed out bad ideas and promote good ones.




πŸ“ Justified (Bite code!). just is a Rust-based task runner and here's a good write up about its features. If I wasn't writing ds (my own task runner), I'd probably use just. I'll probably experimentally support some subset of just's syntax in ds.


πŸ“ Book Time #26: How I Read (Aaron Gordon / Book Time). My process for selecting books is very different from Aaron's, but it's nice to see someone else's process.

Mine:

  • I get recommendations from friends, colleagues, podcasts, and other books.
  • I add them to a list (currently an Amazon Wishlist). I prefer digital books over physical books because I can make them into audiobook (at 4x) and they take up very little physical space.
  • When I get to the end of the books I have in my queue, I sort the wishlist by price (lowest to highest) and buy everything under $5. That keeps me busy for about 3 months.
  • I take all the books that I've purchased and sort them by the number of pages (lowest to highest). This is my new reading queue.

There are only a couple of exceptions for me to deviate from this order:

  1. My wife recommends I read something next, especially as it may impact our kids. This bumps the book to the top of the list (both for purchase and reading).
  2. I'm about to meet someone who recently published a book. There have been a few times where I got through their most-recently published book the day before meeting them. HT: Ramit Sethi who teaches the importance of being overly prepared for certain kinds of meetings.

I often get asked how much I retain from the audio versions of books. My informal tests show no more or less than most people when they read books. I don't remember every detail, but neither do most people. HT: Mike Masnick who introduced me to the idea of listening to podcasts at greater than 2x.




TIL: importlib.metadata.version #

Apparently __version__ can be dynamic now.

While I was updating my standard pyproject.toml to use dependency groups, I switched to using uv as my build backend (from setuptools). Like all the other design choices, it has very sensible defaults with good overrides when you need them.

One thing that it doesn't support is dynamic versions (i.e. reading __version__ from __init__.py). Charlie Marsh explains:

Using dynamic metadata for things that are actually just static lookups feels like the wrong tradeoff.

Like version = ["dynamic"] to read the version from __init__.py.

As soon as you have dynamic metadata, you need to install dependencies and run Python just to get that info.


Static metadata rules. Just write the version out twice!

This makes sense, but I really didn't want to write out the version twice. Luckily, uv has a nice uv version command to change/bump the version number in the pyproject.toml, so I started thinking about how I'd also change the version in the __init__.py at the same time.

I asked ChatGPT what the best approach was and it suggested something quite different: don't write the value in __init__.py at all.

from importlib.metadata import version, PackageNotFoundError
try:
    __version__ = version("package-name") # from pyproject.toml: project.name
except PackageNotFoundError:
    __version__ = "0.0.0" # aka unknown version

I have since noticed that this was also the suggestion Adam Johnson had in the same thread.

One minor concern I have is whether this works in a cosmofy build, but that will be a separate exploration.


Dependency Groups to the Rescue #

Finally a standard place to put dev dependencies.

Previously: Stop Hiding Python Dev Dependencies

I'm a bit late to the party, but even when I saw that PEP 735 – Dependency Groups in pyproject.toml had been accepted and standardized by PyPA, it still didn't register how this should impact my pyproject.toml configurations.

In my previous post, I argued that absent a standard place to put dev dependencies in pyproject.toml, we should opt to use optional-dependencies. However, dependency groups seem to offer a nice standard place for such dependencies: these are bundles of dependencies that don't get built into the final distribution (i.e. they are not required to run the package).

Feels like the best of both worlds and uv already started using dependency-groups.dev as the place that uv add --dev writes to and this group is sync'd by default when using uv sync and uv run.

I only really got this when I read Simon Willison's post about how he's using dependency groups to make it easier for people to hack on his code.


πŸ“– Remix: Making Art and Commerce Thrive in the Hybrid Economy by Lawrence Lessig (2002; via Tribe of Mentors & Joseph Gordon Levitt). I knew most of these ideas from having read TechDirt for so many years and listening to Lessig talk about these ideas for several decades. Still, I was surprised how much of the fights we have today still stem from a basic disagreement about copyright and how art gets made.







castfit 0.1.3 #

Now with a non-exhaustive is_subtype function

castfit 0.1.3 is available. The biggest thing I did was implement is_subtype which is a forced me to learn about invariant, covariant, and contravariant types.

To install castfit:

# modern (recommended)
uv add castfit

# classic
python -m pip install castfit

Read more


Release Notes 0.1.3 - 2025-11-27T03:12:11Z #

Changed

  • #33 PyPI publishing to support newer uv build metadata
  • #40 PyPI publishing to use uv publish instead of pypa/gh-action-pypi-publish

Added

  • #23 ability to type check Callable via is_subtype
  • #34 support for property fields on classes
  • #35 __name__ to TypeInfo if present
  • #36 support for default values in dataclasses.Field
  • #37 support for function definitions in classes

Removed

  • #40 support for python 3.9

Security

  • #40 We are trying out uv publish instead of pypa/gh-action-pypi-publish.


πŸ“– Remote: Office Not Required by Jason Fried & David Heinemeier Hansson (2013; via Iridescent Learning). It's interesting to see how work from home policies have ebbed and flowed since 2013.






View all posts