mirror of
https://github.com/jbranchaud/til
synced 2026-07-06 09:10:34 +00:00
Compare commits
14 Commits
03c11f9042
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d4766dad95 | |||
| 5f4308c1af | |||
| 8c463a90e3 | |||
| 329267c7a2 | |||
| 12180acb02 | |||
| 8f722061b4 | |||
| a126b13d7c | |||
| 0ae3f14c25 | |||
| b91527db30 | |||
| c8f8c2c1a3 | |||
| c397e35ffd | |||
| bbb28fd811 | |||
| 950e2f861a | |||
| 20bbdb6d55 |
@@ -10,7 +10,7 @@ working across different projects via [VisualMode](https://www.visualmode.dev/).
|
||||
|
||||
For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter).
|
||||
|
||||
_1798 TILs and counting..._
|
||||
_1810 TILs and counting..._
|
||||
|
||||
See some of the other learning resources I work on:
|
||||
|
||||
@@ -60,6 +60,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
* [Linux](#linux)
|
||||
* [LLM](#llm)
|
||||
* [Mac](#mac)
|
||||
* [Math](#math)
|
||||
* [Mise](#mise)
|
||||
* [MongoDB](#mongodb)
|
||||
* [MySQL](#mysql)
|
||||
@@ -470,6 +471,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [List PRs Awaiting Your Review](github/list-prs-awaiting-your-review.md)
|
||||
- [Open A PR To An Unforked Repo](github/open-a-pr-to-an-unforked-repo.md)
|
||||
- [Open File To Specific Line In Browser](github/open-file-to-specific-line-in-browser.md)
|
||||
- [Process JSON Output From gh With jq](github/process-json-output-from-gh-with-jq.md)
|
||||
- [Target Another Repo When Creating A PR](github/target-another-repo-when-creating-a-pr.md)
|
||||
- [Tell gh What The Default Repo Is](github/tell-gh-what-the-default-repo-is.md)
|
||||
|
||||
@@ -770,6 +772,10 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [View All Windows Of The Current App](mac/view-all-windows-of-the-current-app.md)
|
||||
- [Write System Clipboard To A File](mac/write-system-clipboard-to-a-file.md)
|
||||
|
||||
### Math
|
||||
|
||||
- [Generate Permutations Of All Valid 9-ball Racks](math/generate-permutations-of-all-valid-9-ball-racks.md)
|
||||
|
||||
### Mise
|
||||
|
||||
- [Create Umbrella Task For All Test Tasks](mise/create-umbrella-task-for-all-test-tasks.md)
|
||||
@@ -1068,8 +1074,10 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [Create A Range Of Descending Values](python/create-a-range-of-descending-values.md)
|
||||
- [Deduplicate A List Into A Tuple](python/deduplicate-a-list-into-a-tuple.md)
|
||||
- [Define Sequence Of Tests With Parametrize Decorator](python/define-sequence-of-tests-with-parametrize-decorator.md)
|
||||
- [Define Typed Class Interface With Protocol](python/define-typed-class-interface-with-protocol.md)
|
||||
- [Dunder Methods](python/dunder-methods.md)
|
||||
- [Easy Key-Value Aggregates With defaultdict](python/easy-key-value-aggregates-with-defaultdict.md)
|
||||
- [Enable Pyright Type Checking In Cursor](python/enable-pyright-type-checking-in-cursor.md)
|
||||
- [Get Absolute Seconds From `timedelta` Object](python/get-absolute-seconds-from-timedelta-object.md)
|
||||
- [Get Quotient And Remainder In One Operation](python/get-quotient-and-remainder-in-one-operation.md)
|
||||
- [Install With PIP For Specific Interpreter](python/install-with-pip-for-specific-interpreter.md)
|
||||
@@ -1079,15 +1087,18 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [Load A File Into The Python REPL](python/load-a-file-into-the-python-repl.md)
|
||||
- [Look Inside Pytest tmp_path](python/look-inside-pytest-tmp-path.md)
|
||||
- [Make Dataclass Sortable By Specific Field](python/make-dataclass-sortable-by-specific-field.md)
|
||||
- [Make Secure Temp File For Atomic Write](python/make-secure-temp-file-for-atomic-write.md)
|
||||
- [Override The Boolean Context Of A Class](python/override-the-boolean-context-of-a-class.md)
|
||||
- [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md)
|
||||
- [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md)
|
||||
- [Set Up Pyright Type Checking In GitHub](python/set-up-pyright-type-checking-in-github.md)
|
||||
- [Skip Specific Pytest Test Cases](python/skip-specific-pytest-test-cases.md)
|
||||
- [Sort A List Of Dataclass Instances](python/sort-a-list-of-dataclass-instances.md)
|
||||
- [Sort Normalized Version Of Data](python/sort-normalized-version-of-data.md)
|
||||
- [Start The Debugger When A Test Errors](python/start-the-debugger-when-a-test-errors.md)
|
||||
- [Store And Access Immutable Data In A Tuple](python/store-and-access-immutable-data-in-a-tuple.md)
|
||||
- [Test A Function With Pytest](python/test-a-function-with-pytest.md)
|
||||
- [Turn Method Into Cached Property On Class Instance](python/turn-method-into-cached-property-on-class-instance.md)
|
||||
- [Use pipx To Install End User Apps](python/use-pipx-to-install-end-user-apps.md)
|
||||
- [Use `__post_init__` For `dataclass` Validations](python/use-post-init-for-dataclass-validations.md)
|
||||
- [Use Verbose Flag To Get More Diff](python/use-verbose-flag-to-get-more-diff.md)
|
||||
@@ -1147,6 +1158,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [Customize Paths And Helpers For Devise Routes](rails/customize-paths-and-helpers-for-devise-routes.md)
|
||||
- [Customize Template For New Schema Migration](rails/customize-template-for-new-schema-migration.md)
|
||||
- [Customize The Path Of A Resource Route](rails/customize-the-path-of-a-resource-route.md)
|
||||
- [Define Conditional Routing Logic In Routes File](rails/define-conditional-routing-logic-in-routes-file.md)
|
||||
- [Define The Root Path For The App](rails/define-the-root-path-for-the-app.md)
|
||||
- [Delete Paranoid Records](rails/delete-paranoid-records.md)
|
||||
- [Demodulize A Class Name](rails/demodulize-a-class-name.md)
|
||||
@@ -1563,6 +1575,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [Update The Gemfile Bundled With Version](ruby/update-the-gemfile-bundled-with-version.md)
|
||||
- [Use A Case Statement As A Cond Statement](ruby/use-a-case-statement-as-a-cond-statement.md)
|
||||
- [Use dotenv In A Non-Rails Project](ruby/use-dotenv-in-a-non-rails-project.md)
|
||||
- [Use Rescue As Part Of Inline Statement](ruby/use-rescue-as-part-of-inline-statement.md)
|
||||
- [Use Tap For Better Test Data Setup](ruby/use-tap-for-better-test-data-setup.md)
|
||||
- [Using BCrypt To Create And Check Hashed Passwords](ruby/using-bcrypt-to-create-and-check-hashed-passwords.md)
|
||||
- [What To Do When You Don't Rescue](ruby/what-to-do-when-you-dont-rescue.md)
|
||||
@@ -1711,6 +1724,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [Curl With Cookies](unix/curl-with-cookies.md)
|
||||
- [Curling For Headers](unix/curling-for-headers.md)
|
||||
- [Curling With Basic Auth Credentials](unix/curling-with-basic-auth-credentials.md)
|
||||
- [Deduplicate List While Preserving Original Order](unix/deduplicate-list-while-preserving-original-order.md)
|
||||
- [Determine ipv4 And ipv6 Public IP Addresses](unix/determine-ipv4-and-ipv6-public-ip-addresses.md)
|
||||
- [Diff Two Files In Unified Format](unix/diff-two-files-in-unified-format.md)
|
||||
- [Different Ways To Generate A v4 UUID](unix/different-ways-to-generate-a-v4-uuid.md)
|
||||
@@ -1804,6 +1818,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [Print A Range Of Lines For A File With Bat](unix/print-a-range-of-lines-for-a-file-with-bat.md)
|
||||
- [Print DateTime Represented By Unix Timestamp](unix/print-datetime-represented-by-unix-timestamp.md)
|
||||
- [Print Milliseconds In Human-Readable Format](unix/print-milliseconds-in-human-readable-format.md)
|
||||
- [Print Out File With Bat Without Formatting](unix/print-out-file-with-bat-without-formatting.md)
|
||||
- [Print Out Files In Reverse](unix/print-out-files-in-reverse.md)
|
||||
- [Print The Current Date In Human-Readable Format](unix/print-the-current-date-in-human-readable-format.md)
|
||||
- [Produce A Lowercase V4 UUID](unix/produce-a-lowercase-v4-uuid.md)
|
||||
@@ -2078,6 +2093,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
||||
- [Send A Message To A Discord Channel](workflow/send-a-message-to-a-discord-channel.md)
|
||||
- [Send A PDF To Your Kindle](workflow/send-a-pdf-to-your-kindle.md)
|
||||
- [Set Recurring Reminders In Slack](workflow/set-recurring-reminders-in-slack.md)
|
||||
- [Show All Linear Keyboard Shortcuts](workflow/show-all-linear-keyboard-shortcuts.md)
|
||||
- [Show Linting Errors In Zed](workflow/show-linting-errors-in-zed.md)
|
||||
- [Temporarily Hide CleanShot X Capture Previews](workflow/temporarily-hide-cleanshot-x-capture-previews.md)
|
||||
- [Toggle Between Stories In Storybook](workflow/toggle-between-stories-in-storybook.md)
|
||||
|
||||
@@ -11,6 +11,36 @@ tasks:
|
||||
cmds:
|
||||
- task --list
|
||||
|
||||
browse:list:
|
||||
desc: Print deduped, newest-first TIL paths
|
||||
silent: true
|
||||
cmds:
|
||||
- |
|
||||
git log --diff-filter=A --name-only --pretty=format: -- '*/*.md' \
|
||||
| grep -v '^$' \
|
||||
| awk '!seen[$0]++'
|
||||
|
||||
browse:
|
||||
desc: Pick from 5 most recent TILs (fzf) and open in browser
|
||||
interactive: true
|
||||
silent: true
|
||||
cmds:
|
||||
- |
|
||||
FILE=$(task browse:list | head -5 | fzf --prompt="Open TIL: " --height=40% --reverse) || true
|
||||
if [ -n "$FILE" ]; then
|
||||
gh browse "$FILE"
|
||||
fi
|
||||
|
||||
browse:latest:
|
||||
desc: Open the single most recent TIL in the browser
|
||||
silent: true
|
||||
cmds:
|
||||
- |
|
||||
FILE=$(task browse:list | head -1)
|
||||
if [ -n "$FILE" ]; then
|
||||
gh browse "$FILE"
|
||||
fi
|
||||
|
||||
notes:
|
||||
desc: Interactive picker for notes tasks
|
||||
cmds:
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Process JSON Output From gh With jq
|
||||
|
||||
The `gh` (GitHub) CLI is useful for accessing data about your profile and
|
||||
projects from the terminal. With the `--json` flag, we can access the data in a
|
||||
structured way which is useful for scripting.
|
||||
|
||||
Here is an example of pulling a list of all my repositories, limiting each
|
||||
entity to just the `nameWithOwner` and `description`:
|
||||
|
||||
```bash
|
||||
❯ gh repo list --limit 1000 --json nameWithOwner,description
|
||||
[
|
||||
{
|
||||
"description": "My personal site -- joshbranchaud.com",
|
||||
"nameWithOwner": "jbranchaud/personal-site"
|
||||
},
|
||||
{
|
||||
"description": "Private repo for the NOTES.md of my TIL repo",
|
||||
"nameWithOwner": "jbranchaud/til-notes-private"
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
If I'm using the `--json` flag, then I can add in the `--jq` flag to apply a
|
||||
`jq` query for additional processing of the output.
|
||||
|
||||
Here I convert it to a series of tuples:
|
||||
|
||||
```bash
|
||||
❯ gh repo list --limit 1000 --json nameWithOwner,description \
|
||||
--jq '.[] | [.nameWithOwner, .description]'
|
||||
[
|
||||
"jbranchaud/personal-site",
|
||||
"My personal site -- joshbranchaud.com"
|
||||
]
|
||||
[
|
||||
"jbranchaud/til-notes-private",
|
||||
"Private repo for the NOTES.md of my TIL repo"
|
||||
]
|
||||
...
|
||||
```
|
||||
|
||||
Then I can add one more pipe to that `jq` query to turn it into _tab-separated
|
||||
values_ using
|
||||
[`@tsv`](https://jqlang.org/manual/v1.5/#format-strings-and-escaping):
|
||||
|
||||
```bash
|
||||
❯ gh repo list --limit 1000 --json nameWithOwner,description \
|
||||
--jq '.[] | [.nameWithOwner, .description] | @tsv'
|
||||
jbranchaud/personal-site My personal site -- joshbranchaud.com
|
||||
jbranchaud/til-notes-private Private repo for the NOTES.md of my TIL repo
|
||||
...
|
||||
```
|
||||
|
||||
This is useful because I can then pipe it to another program, such as an `fzf`
|
||||
command like [this repo selector that opens the selected one in the
|
||||
browser](https://github.com/jbranchaud/dotfiles/commit/f964ca10c6c4db3475411c2991dc2f1dfd18c818).
|
||||
@@ -0,0 +1,42 @@
|
||||
# Generate Permutations Of All Valid 9-ball Racks
|
||||
|
||||
I wanted to produce a full listing of all valid rack arrangements for the game
|
||||
of [9-ball](https://en.wikipedia.org/wiki/Nine-ball). The constraints on how a
|
||||
9-ball rack can be arranged are, first, that the 1 ball must be placed at the
|
||||
head of the diamond and, second, that the 9 ball must be placed at the center of
|
||||
the diamond. After that, all other balls (2 through 8) can be placed in any
|
||||
arrangement.
|
||||
|
||||
Because each of those seven remaining balls can be arranged in distinct
|
||||
orderings where each ball is placed once, this is a
|
||||
[_permutation_](https://en.wikipedia.org/wiki/Permutation) problem.
|
||||
|
||||
> In elementary combinatorics, the k-permutations, or partial permutations, are
|
||||
> the ordered arrangements of k distinct elements selected from a set. When k is
|
||||
> equal to the size of the set, these are the permutations in the previous
|
||||
> sense.
|
||||
|
||||
For this problem, the seven distinct elements can be arranged into `7!` (seven
|
||||
factorial) unique permutations. That is, 5040 permutations.
|
||||
|
||||
I can use [Ruby's `Array#permutations`
|
||||
method](https://docs.ruby-lang.org/en/4.0/Array.html#method-i-permutation) to
|
||||
enumerate these 5040 permutations like so:
|
||||
|
||||
```ruby
|
||||
[2,3,4,5,6,7,8].permutation.map do |perm|
|
||||
[1, *perm[0..2], 9, *perm[3..7]]
|
||||
end.to_a
|
||||
=> [[1, 2, 3, 4, 9, 5, 6, 7, 8],
|
||||
[1, 2, 3, 4, 9, 5, 6, 8, 7],
|
||||
[1, 2, 3, 4, 9, 5, 7, 6, 8],
|
||||
[1, 2, 3, 4, 9, 5, 7, 8, 6],
|
||||
[1, 2, 3, 4, 9, 5, 8, 6, 7],
|
||||
[1, 2, 3, 4, 9, 5, 8, 7, 6],
|
||||
[1, 2, 3, 4, 9, 6, 5, 7, 8],
|
||||
...
|
||||
[1, 8, 7, 6, 9, 5, 3, 2, 4],
|
||||
[1, 8, 7, 6, 9, 5, 3, 4, 2],
|
||||
[1, 8, 7, 6, 9, 5, 4, 2, 3],
|
||||
[1, 8, 7, 6, 9, 5, 4, 3, 2]]
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
# Define Typed Class Interface With Protocol
|
||||
|
||||
In [`py-vmt`](https://github.com/jbranchaud/py-vmt) I am defining different
|
||||
storage access layers for the CLI to use. I want a consistent interface that the
|
||||
core CLI logic can depend on regardless of whether it is a JSON file or a SQLite
|
||||
database. To achieve that I can define a class of unimplemented functions that
|
||||
inherits from
|
||||
[`typing.Protocol`](https://typing.python.org/en/latest/spec/protocol.html).
|
||||
|
||||
```python
|
||||
from typing import Protocol
|
||||
|
||||
class SessionRepository(Protocol):
|
||||
def active_session(self) -> Session | None: ...
|
||||
def write_active_session(self, session) -> None: ...
|
||||
def append_session(self, session) -> None: ...
|
||||
def all_sessions(self) -> list[Session]: ...
|
||||
def clear_active_session(self) -> None: ...
|
||||
```
|
||||
|
||||
Notice that none of these have default implementations. The `...` indicates that
|
||||
class implementing this protocol will define the implementation of those
|
||||
functions.
|
||||
|
||||
Now, my `CliContext` class, which needs some kind of `SessionRepository` to
|
||||
function can indicate as much in `__init__`.
|
||||
|
||||
```python
|
||||
class CliContext:
|
||||
def __init__(self, verbose: bool, repo: SessionRepository | None = None) -> None:
|
||||
self.verbose: bool = verbose
|
||||
self.active_session: Session | None = None
|
||||
self.repo: SessionRepository = repo or JsonRepository()
|
||||
self.active_session = self.repo.active_session()
|
||||
```
|
||||
|
||||
If `JsonRepository` doesn't define all of the methods specified in the protocol,
|
||||
then a type error will occur wherever it clashes with `SessionRepository`. Now
|
||||
as I implement `SqliteRepository` I have a standard interface to build against
|
||||
that I know I can seamlessly swap in.
|
||||
|
||||
[source](https://typing.python.org/en/latest/reference/protocols.html#simple-user-defined-protocols)
|
||||
@@ -0,0 +1,24 @@
|
||||
# Enable Pyright Type Checking In Cursor
|
||||
|
||||
In most ways [Cursor](https://cursor.com/), a clone of VS Code, behaves like VS
|
||||
Code and uses the same extensions as VS Code. It even offers to clone all your
|
||||
existing extensions and setup from VS Code when you first install it.
|
||||
|
||||
However, the Pyright type checking setup that I had in VS Code stopped working
|
||||
when I opened up the same Python project in Cursor. It seems that to get Pyright
|
||||
to reliably work with forks of VS Code, you need to use a compatible fork like
|
||||
[Based Pyright](https://docs.basedpyright.com/latest/).
|
||||
|
||||
Once I installed _Based Pyright_ from the extension marketplace, I was able to
|
||||
enable it in `.vscode/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
...,
|
||||
"basedpyright.analysis.typeCheckingMode": "basic"
|
||||
}
|
||||
```
|
||||
|
||||
I may have needed to restart Cursor at this point, I cannot remember exactly.
|
||||
However, once this setup was in place the helpful type checking errors started
|
||||
appearing as red squiggles.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Make Secure Temp File For Atomic Write
|
||||
|
||||
Two types of failure modes that can occur while writing to a shared file on the
|
||||
file system are 1) a corrupted file due to a crash mid-write and 2) another
|
||||
process reading a partial file mid-write.
|
||||
|
||||
One way I've handled this in [`py-vmt`](https://github.com/jbranchaud/py-vmt) is
|
||||
to perform the write operations on a secure temp file and then use the OS-level
|
||||
atomic `rename` operation. I do this by [creating a
|
||||
`contextmanager`](https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager)
|
||||
that uses
|
||||
[`tempfile.mkstemp`](https://docs.python.org/3/library/tempfile.html#tempfile.mkstemp)
|
||||
and [`os.replace`](https://docs.python.org/3/library/os.html#os.replace).
|
||||
|
||||
Here is what the `contextmanager` looks like:
|
||||
|
||||
```python
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
import os, tempfile
|
||||
|
||||
@contextmanager
|
||||
def atomic_write(path: Path):
|
||||
# write to a tmp file in the same directory, then atomically swap it
|
||||
fd, temp_file_path = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
|
||||
try:
|
||||
with os.fdopen(fd, "w") as file:
|
||||
yield file
|
||||
os.replace(temp_file_path, path)
|
||||
except BaseException:
|
||||
os.unlink(temp_file_path)
|
||||
raise
|
||||
```
|
||||
|
||||
This explicitly creates a secure temp file in the same directory as the given
|
||||
path with `.tmp` as the suffix. I then open the file descriptor using the
|
||||
`os.fdopen` context manager (which will manage closing the file descriptor for
|
||||
me). The `@contextmanager` decorator plus the `yield file` are what allow this
|
||||
to be used as a `with` block. Once any file operations are done, then I use
|
||||
`os.replace` to atomically swap out the original file with the temp file.
|
||||
|
||||
Here is how I use it to write updates to JSON data files:
|
||||
|
||||
```python
|
||||
def write_active_session(self, session: Session) -> None:
|
||||
with atomic_write(self.active_session_file) as file:
|
||||
json.dump(session.marshal(), file)
|
||||
```
|
||||
@@ -0,0 +1,62 @@
|
||||
# Set Up Pyright Type Checking In GitHub
|
||||
|
||||
As I get into more of a PR workflow with my development of
|
||||
[`py-vmt`](https://github.com/jbranchaud/py-vmt), I need to set up some basic CI
|
||||
checks in GitHub. For starters I want the same `pyright` type checking that I
|
||||
have locally to be run in CI for consistency.
|
||||
|
||||
Though my editor is set up to do Pyright type checking as I work locally, I can
|
||||
also manually run it with:
|
||||
|
||||
```bash
|
||||
$ uv run pyright
|
||||
```
|
||||
|
||||
Pyright will look for the `tool.pyright` section in my `pyproject.toml` file
|
||||
which currently looks like the following:
|
||||
|
||||
```toml
|
||||
[tool.pyright]
|
||||
include = ["src", "tests"]
|
||||
```
|
||||
|
||||
I can get this same type checking in CI for PRs by adding the following
|
||||
`.github/workflows/typecheck.yml` file:
|
||||
|
||||
```yaml
|
||||
name: pyright
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Run pyright
|
||||
run: uv run pyright
|
||||
```
|
||||
|
||||
This adds a single `typecheck` job that installs `uv`, `python`, and my project
|
||||
dependencies, and then runs `uv run pyright` (just like I do locally) to perform
|
||||
type checking. If `pyright` discovers any type errors, the job will fail and I
|
||||
can view the output of the job to see what needs fixing. Once I have dealt with
|
||||
everything, the job will quietly pass with a green check mark.
|
||||
|
||||
Here is [the PR](https://github.com/jbranchaud/py-vmt/pull/2) where I added this
|
||||
CI job.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Turn Method Into Cached Property On Class Instance
|
||||
|
||||
I have a class that encapsulates a few things including a somewhat expensive
|
||||
data lookup from a file on disk. When this class is instantiated, it is
|
||||
short-lived and the data that gets pulled from the file on disk is considered
|
||||
fresh for the life of the instance.
|
||||
|
||||
```python
|
||||
class CliContext:
|
||||
def __init__(self, verbose: bool) -> None:
|
||||
# ...
|
||||
self.repo = JsonRepository()
|
||||
# ...
|
||||
|
||||
def session_log(self) -> list[Session]:
|
||||
return self.repo.load_session_log()
|
||||
```
|
||||
|
||||
Because this method gets called from a couple places during a single lifecycle,
|
||||
this class would benefit from caching it via the [`@cached_property`
|
||||
decorator](https://docs.python.org/3/library/functools.html#functools.cached_property).
|
||||
|
||||
```python
|
||||
from functools import cached_property
|
||||
|
||||
class CliContext:
|
||||
def __init__(self, verbose: bool) -> None:
|
||||
# ...
|
||||
self.repo = JsonRepository()
|
||||
# ...
|
||||
|
||||
@cached_property
|
||||
def session_log(self) -> list[Session]:
|
||||
return self.repo.load_session_log()
|
||||
```
|
||||
|
||||
Now `session_log` can be treated like a property instead of a method. That means
|
||||
when I want to load and access the session log, I can do `self.session_log` (no
|
||||
parentheses) like I would any other property. The first time I reference it, the
|
||||
method will run. Then that value will be cached and all subsequent references
|
||||
will use that cache.
|
||||
|
||||
> Transform a method of a class into a property whose value is computed once and
|
||||
> then cached as a normal attribute for the life of the instance.
|
||||
|
||||
Of course, anytime we use caching, we can create a footgun for ourselves. We
|
||||
have to be careful that our program doesn't evolve in such a way where the
|
||||
caching will create a subtle bug due to stale data.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Define Conditional Routing Logic In Routes File
|
||||
|
||||
I ran into a situation recently where I needed to intercept the behavior a
|
||||
common public-facing route in an app. Broadly, the route is for company specific
|
||||
rental pages with query parameters that correspond to their available inventory.
|
||||
|
||||
What I needed was a way to display a demo version of that rental page ignoring
|
||||
everything else about how the request would otherwise be processed, validated,
|
||||
and rendered.
|
||||
|
||||
Instead of introducing a bunch of weird conditional logic into this already
|
||||
complex rental controller, I was able to intercept the request at the routing
|
||||
layer when `demo=true` is set and send it to a different controller.
|
||||
|
||||
Here is what that section of `config/routes.rb` looks like:
|
||||
|
||||
```ruby
|
||||
get "rentals/new", to: "rental_demos#show",
|
||||
as: :rental_demo,
|
||||
constraints: ->(request) { request.params[:demo] == "true" }
|
||||
|
||||
resources :rentals, only: %i[new create] do
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
This specifies a `constraint` on the `get` handler matching for a given request.
|
||||
If the constraint isn't met, then the route handling logic proceeds where it
|
||||
will instead find a match with the original new rentals resource routing.
|
||||
|
||||
Now I can reference a version of this URL that includes `demo=true` as a way of
|
||||
having an always-available realistic-looking version of the rental page even if
|
||||
one of these companies doesn't actively have available inventory.
|
||||
|
||||
Those requests will get intercepted by the first matching route handler which
|
||||
will send them to the `RentalDemosController` instead of the
|
||||
`RentalsController`.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Use Rescue As Part Of Inline Statement
|
||||
|
||||
In Ruby I typically think of `rescue` as block syntax that I can use to handle
|
||||
exceptions.
|
||||
|
||||
```ruby
|
||||
begin
|
||||
User.update!(password:)
|
||||
rescue
|
||||
puts "There was an issue updating the password"
|
||||
end
|
||||
```
|
||||
|
||||
The `rescue` keyword can also be used as part of an inline statement as a way of
|
||||
providing a _fallback_ value when the first part of the statement raises.
|
||||
|
||||
For instance, if I'm trying to access some value on an array that happens to be
|
||||
`nil`, it is going to raise:
|
||||
|
||||
```ruby
|
||||
> scores.first
|
||||
(irb):7:in '<main>': undefined method 'first' for nil (NoMethodError)
|
||||
```
|
||||
|
||||
I can instead tack on a `rescue 0` which will give it `0` as a fallback value:
|
||||
|
||||
```ruby
|
||||
> scores.first rescue 0
|
||||
=> 0
|
||||
```
|
||||
|
||||
Of course, there are more idiomatic ways to handle this kind of situation in
|
||||
Ruby. Maybe something like this:
|
||||
|
||||
```ruby
|
||||
> Array(scores).first || 0
|
||||
=> 0
|
||||
```
|
||||
|
||||
Another way I've seen this inline rescue used is to print out the exception
|
||||
caused by that line of code, using `$!` (the global variable for the most
|
||||
recently raised exception).
|
||||
|
||||
```ruby
|
||||
> scores.first rescue puts $!
|
||||
undefined method 'first' for nil
|
||||
=> nil
|
||||
```
|
||||
|
||||
That is a one-liner for the following:
|
||||
|
||||
```ruby
|
||||
begin
|
||||
scores.first
|
||||
rescue => e
|
||||
puts e
|
||||
end
|
||||
```
|
||||
|
||||
The big caveat that goes with this is the same one that goes with any other
|
||||
blanket `rescue` block. If you are indiscriminately rescuing exceptions without
|
||||
being intentional about what you are rescuing and why, you could be potentially
|
||||
burying exceptions that you need to know about.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Deduplicate List While Preserving Original Order
|
||||
|
||||
Usually when I want to deduplicate a list coming out of some command, I'll reach
|
||||
for `sort | uniq`. This is a nice Unix trick where `uniq` removes consecutive
|
||||
duplicate lines which relies on `sort` first reorganizing all lines in
|
||||
alphabetically sorted order, bringing all duplicate lines together.
|
||||
|
||||
The caveat to using `sort | uniq` (or even `sort -u`) is that it will reorder
|
||||
entries alphabetically. That means you'll lose the original order, which may
|
||||
have been important.
|
||||
|
||||
```bash
|
||||
❯ echo "red green blue red yellow green blue red green" | tr ' ' '\n' | sort -u
|
||||
blue
|
||||
green
|
||||
red
|
||||
yellow
|
||||
```
|
||||
|
||||
Another approach is to use `awk` which can deduplicate while preserving the
|
||||
order of entries as they first appear. This can be done with a pattern that
|
||||
records the count of each line in an associative array.
|
||||
|
||||
```bash
|
||||
❯ echo "red green blue red yellow green blue red green" | tr ' ' '\n' | awk '!seen[$0]++'
|
||||
red
|
||||
green
|
||||
blue
|
||||
yellow
|
||||
```
|
||||
|
||||
The above pattern accepts on the first occurrence of each line and rejects on
|
||||
any subsequent occurrences. That is done by adding `$0` (the current line) to
|
||||
`seen` (associative array that auto-initializes inline). If it doesn't exist in
|
||||
`seen` yet, then `0` is returned which is negated to a truthy value with `!`.
|
||||
That entry is then incremented from `0` to `1` via the `++`. As `awk` continues
|
||||
to process each line, `seen` is continually added to and incremented. The
|
||||
default _action_ for `awk` is to print the line. Those truthy lines are the ones
|
||||
that are printed.
|
||||
|
||||
An example of where this might be useful is when creating a unique listing of
|
||||
all authors of a git repository while maintaining the order that they become
|
||||
committers. I wanted to show this with a high-contribution public repo that I
|
||||
worked on, so I referenced the [`egghead-next`
|
||||
repo](https://github.com/skillrecordings/egghead-next).
|
||||
|
||||
```bash
|
||||
❯ git log --reverse --format='%an <%ae>' | awk '!seen[$0]++'
|
||||
Joel Hooks <joelhooks@gmail.com>
|
||||
johnlindquist <johnlindquist@gmail.com>
|
||||
John Lindquist <johnlindquist@gmail.com>
|
||||
William Johnson <w.alexander.johnson@gmail.com>
|
||||
depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
|
||||
Evgeniy Nagalskiy <evgeniy.nagalskiy@gmail.com>
|
||||
Taylor Bell <taylorbell@gmail.com>
|
||||
Maggie Appleton <maggie.fm.appleton@gmail.com>
|
||||
John Lindquist <johnlindquist@work.local>
|
||||
Vojta Holik <vojta@egghead.io>
|
||||
Daniel Miller <dealingwith@gmail.com>
|
||||
jh3y <jh3y@users.noreply.github.com>
|
||||
Jhey Tompkins <jh3y@users.noreply.github.com>
|
||||
Josh Branchaud <jbranchaud@gmail.com>
|
||||
Lauro Silva <57044804+laurosilvacom@users.noreply.github.com>
|
||||
LB <barth.laurie@gmail.com>
|
||||
kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
|
||||
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
|
||||
samuelhulick <samuel@samuelhulick.com>
|
||||
Ian Jones <jones58ian@gmail.com>
|
||||
Zac Jones <zacjones93@gmail.com>
|
||||
...
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
# Print Out File With Bat Without Formatting
|
||||
|
||||
The [`bat`](https://github.com/sharkdp/bat) utility is my daily driver and
|
||||
replacement for anything used `cat` for before. I even have `bat` aliased to
|
||||
`cat` so that I never had to rewire my muscle memory for typing `cat`.
|
||||
|
||||
Whether or not the creator of `cat` intended it, I'd guess that most terminal
|
||||
users' main use case is printing the contents of a file. `bat` does that way
|
||||
better with syntax highlighting, line numbers, and some layout formatting that
|
||||
puts lines around the output and a heading with the filename.
|
||||
|
||||
All this formatting is great when I'm taking a quick look at a file. One way it
|
||||
gets in the way is when I'm trying to highlight and copy a few lines to my
|
||||
clipboard. Because the terminal is rendering lines, line numbers, and other
|
||||
formatting, all that fluff gets included on the clipboard.
|
||||
|
||||
For this scenario, I can use the `-p` flag (or `--style=plain`) to print just
|
||||
the (syntax-highlighted) file contents without all the extra formatting.
|
||||
|
||||
```bash
|
||||
bat -p app/models/users.rb
|
||||
|
||||
# or
|
||||
bat --style=plain app/models/users.rb
|
||||
```
|
||||
|
||||
Another way I could have approached this was to [ignore the alias of `cat` to
|
||||
`bat`](ignore-the-alias-when-running-a-command.md).
|
||||
@@ -0,0 +1,25 @@
|
||||
# Show All Linear Keyboard Shortcuts
|
||||
|
||||
Linear, the project management software, puts an incredible amount of attention
|
||||
to detail into the UX and UI of their app. This includes making the app a power
|
||||
tool for power users with tons of keyboard shortcuts.
|
||||
|
||||
I'm aware of some of Linear's keyboard shortcuts, but the discoverability of
|
||||
many of them is tough.
|
||||
|
||||
A great way to list and browse through all of them right in the app is with
|
||||
`Cmd+/`.
|
||||
|
||||
They are organized into sections that I can scroll through. There is also a
|
||||
search box at the top of this _Keyboard Shortcuts_ panel where I can narrow down
|
||||
the results to those that match a term.
|
||||
|
||||
A few that I'm finding immediately useful are:
|
||||
|
||||
- `gi` to go to my _Inbox_ in the current workspace
|
||||
- `gm` to go to _My Issues_ in the current workspace
|
||||
- `ow` to open a picker to switch between workspaces
|
||||
|
||||
Note: the _Keyboard Shortcuts_ panel lists many of the letter-based shortcuts as
|
||||
being capitalized. I've found that these don't work when I hold shift. For that
|
||||
reason, I've listed the above shortcuts with lowercase letters.
|
||||
Reference in New Issue
Block a user