Since v0.2.0, ytstudio-cli cannot run at all on Windows. src/ytstudio/config.py
does import fcntl at module top level. fcntl is a Unix-only module in the Python
standard library, so the import fails on Windows before any command can execute — including
ytstudio init, ytstudio login, ytstudio status, and every videos / analytics /
comments subcommand.
config.py is imported transitively by basically everything (main.py →
api.py → config.py, and version.py → config.py), so there is no command that works.
Environment
- OS: Windows 11 (cmd / PowerShell)
- Python: 3.12 / 3.13
- ytstudio-cli: every release from v0.2.0 through v0.4.1 (current latest)
- Last working release on Windows: v0.1.1
Reproduction
> ytstudio login
Traceback (most recent call last):
...
File "...\site-packages\ytstudio\main.py", line 6, in <module>
from ytstudio.api import authenticate, get_status
File "...\site-packages\ytstudio\api.py", line 12, in <module>
from ytstudio.config import (
File "...\site-packages\ytstudio\config.py", line 1, in <module>
import fcntl
ModuleNotFoundError: No module named 'fcntl'
When it changed
This is a regression introduced by the profiles refactor.
-
v0.1.1 (last good): config.py had no platform-specific code. It only used
Path.write_text / Path.read_text and Path.mkdir, all cross-platform:
# src/ytstudio/config.py @ v0.1.1
import json
from pathlib import Path
from rich.prompt import Prompt
from ytstudio.ui import console, success_message
CONFIG_DIR = Path.home() / ".config" / "ytstudio-cli"
CLIENT_SECRETS_FILE = CONFIG_DIR / "client_secrets.json"
CREDENTIALS_FILE = CONFIG_DIR / "credentials.json"
def ensure_config_dir() -> None:
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
def save_credentials(credentials: dict) -> None:
ensure_config_dir()
CREDENTIALS_FILE.write_text(json.dumps(credentials, indent=2))
def load_credentials() -> dict | None:
if not CREDENTIALS_FILE.exists():
return None
return json.loads(CREDENTIALS_FILE.read_text())
-
v0.2.0 (regression introduced): the profiles rewrite added import fcntl, an
fcntl.flock-based _config_lock(), and POSIX permission calls. The relevant new code
(unchanged through v0.4.1) is:
# src/ytstudio/config.py @ v0.2.0 .. v0.4.1
import fcntl # <-- Unix only: ModuleNotFoundError on Windows
import json
import os
import re
import shutil
from contextlib import contextmanager
from pathlib import Path
...
def _ensure_private_dir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
path.chmod(0o700) # <-- POSIX permission semantics; no-op on Windows
def _write_private(path: Path, text: str) -> None:
"""Atomically write a secret with owner-only permissions, with no readable window."""
tmp = path.with_name(path.name + ".tmp")
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) # <-- mode ignored on Windows
try:
os.write(fd, text.encode())
os.fsync(fd)
finally:
os.close(fd)
tmp.replace(path)
@contextmanager
def _config_lock():
"""Serialize state mutations and the legacy migration across CLI invocations."""
ensure_config_dir()
lock_path = CONFIG_DIR / ".lock"
with lock_path.open("w") as fh:
try:
fcntl.flock(fh.fileno(), fcntl.LOCK_EX) # <-- Unix only
yield
finally:
fcntl.flock(fh.fileno(), fcntl.LOCK_UN) # <-- Unix only
config.py is the only file in the package that touches fcntl, so the fix is localized.
What needs to change for Windows support
-
Hard blocker — import fcntl (line 1). This alone crashes the import on Windows.
It must not be a top-level unconditional import.
-
_config_lock() uses fcntl.flock. Even with the import guarded, the cross-process
advisory lock needs a Windows code path (msvcrt.locking) or a library.
-
Secondary (won't crash, but the intent breaks silently on Windows):
_ensure_private_dir calls path.chmod(0o700). On Windows chmod only toggles the
read-only bit; 0o700 does nothing meaningful, so the "owner-only" guarantee does not hold.
_write_private / _atomic_write_text pass a POSIX mode (0o600 / 0o644) to os.open;
these bits are ignored on Windows. The atomic-write logic itself is fine cross-platform.
These two are not required to make the CLI run on Windows; they only affect the
permission-hardening intent. They can be guarded with os.name == "posix" and documented,
or left as-is.
Proposed fix
Make the lock and the import cross-platform. Minimal version using only the standard library:
import contextlib
import os
try:
import fcntl # POSIX
except ImportError:
fcntl = None
try:
import msvcrt # Windows
except ImportError:
msvcrt = None
def _lock_file(fh) -> None:
if fcntl is not None:
fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
elif msvcrt is not None:
fh.seek(0)
msvcrt.locking(fh.fileno(), msvcrt.LK_LOCK, 1)
# else: no advisory locking available; degrade gracefully (single-user CLI)
def _unlock_file(fh) -> None:
if fcntl is not None:
fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
elif msvcrt is not None:
fh.seek(0)
msvcrt.locking(fh.fileno(), msvcrt.LK_UNLCK, 1)
@contextlib.contextmanager
def _config_lock():
ensure_config_dir()
lock_path = CONFIG_DIR / ".lock"
with lock_path.open("w") as fh:
_lock_file(fh)
try:
yield
finally:
_unlock_file(fh)
And guard the POSIX permission calls:
def _ensure_private_dir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
if os.name == "posix":
path.chmod(0o700)
(The os.open(..., 0o600) mode bits in _write_private are already ignored on Windows,
so no crash — but for clarity you may want a comment noting the permission guarantee is
POSIX-only.)
Alternatively, a single dependency such as filelock
gives a robust cross-platform lock and removes all the platform branching:
from filelock import FileLock
@contextlib.contextmanager
def _config_lock():
ensure_config_dir()
with FileLock(str(CONFIG_DIR / ".lock")):
yield
Offer
If you're open to it, I'm happy to put together a PR implementing the cross-platform locking
(either the stdlib msvcrt approach or filelock, your preference) plus the guarded
permission calls, and verify it on Windows. Just let me know which direction you'd like and
I'll have it coded up.
Since v0.2.0,
ytstudio-clicannot run at all on Windows.src/ytstudio/config.pydoes
import fcntlat module top level.fcntlis a Unix-only module in the Pythonstandard library, so the import fails on Windows before any command can execute — including
ytstudio init,ytstudio login,ytstudio status, and everyvideos/analytics/commentssubcommand.config.pyis imported transitively by basically everything (main.py→api.py→config.py, andversion.py→config.py), so there is no command that works.Environment
Reproduction
When it changed
This is a regression introduced by the profiles refactor.
v0.1.1 (last good):
config.pyhad no platform-specific code. It only usedPath.write_text/Path.read_textandPath.mkdir, all cross-platform:v0.2.0 (regression introduced): the profiles rewrite added
import fcntl, anfcntl.flock-based_config_lock(), and POSIX permission calls. The relevant new code(unchanged through v0.4.1) is:
config.pyis the only file in the package that touchesfcntl, so the fix is localized.What needs to change for Windows support
Hard blocker —
import fcntl(line 1). This alone crashes the import on Windows.It must not be a top-level unconditional import.
_config_lock()usesfcntl.flock. Even with the import guarded, the cross-processadvisory lock needs a Windows code path (
msvcrt.locking) or a library.Secondary (won't crash, but the intent breaks silently on Windows):
_ensure_private_dircallspath.chmod(0o700). On Windowschmodonly toggles theread-only bit;
0o700does nothing meaningful, so the "owner-only" guarantee does not hold._write_private/_atomic_write_textpass a POSIX mode (0o600/0o644) toos.open;these bits are ignored on Windows. The atomic-write logic itself is fine cross-platform.
These two are not required to make the CLI run on Windows; they only affect the
permission-hardening intent. They can be guarded with
os.name == "posix"and documented,or left as-is.
Proposed fix
Make the lock and the import cross-platform. Minimal version using only the standard library:
And guard the POSIX permission calls:
(The
os.open(..., 0o600)mode bits in_write_privateare already ignored on Windows,so no crash — but for clarity you may want a comment noting the permission guarantee is
POSIX-only.)
Alternatively, a single dependency such as
filelockgives a robust cross-platform lock and removes all the platform branching:
Offer
If you're open to it, I'm happy to put together a PR implementing the cross-platform locking
(either the stdlib
msvcrtapproach orfilelock, your preference) plus the guardedpermission calls, and verify it on Windows. Just let me know which direction you'd like and
I'll have it coded up.