Skip to content

Outdated/Incorrect annotations for os.startfile #10991

Closed

Description

@Jakob-Stadler

In my opinion, the annotations for os.startfile have major problems that have been overlooked for the past few years.

For reference, the current annotations in stdlib/os__init__.pyi

if sys.platform == "win32":
    if sys.version_info >= (3, 8):
        def startfile(path: StrOrBytesPath, operation: str | None = None) -> None: ...
    else:
        def startfile(filepath: StrOrBytesPath, operation: str | None = None) -> None: ...

1. Renaming the first argument for filepath to path in >= 3.8

Is there reason why this change has been added? While the official docs list the argument name as path (docs), the CPython source code has the first argument as filepath (CPython/main), and firing up a REPL to check actual behavior mirrors the same:

>>> from os import startfile
>>> startfile(path='explorer')
Traceback (most recent call last):
  File "<pyshell#1>", line 1, in <module>
    startfile(path='explorer')
TypeError: startfile() missing required argument 'filepath' (pos 1)

To me, it looks like a case of incorrect documentation that has been transferred into type annotations without checking whether it mirrors runtime behaviour.

2. Allowing None for operation argument

This is another annotation that conflicts with runtime behaviour:

>>> from os import startfile
>>> startfile(filepath='explorer', operation=None)
Traceback (most recent call last):
  File "<pyshell#3>", line 1, in <module>
    startfile(filepath='explorer', operation=None)
TypeError: startfile() argument 2 must be str, not None

I'll be honest, I'm not 100% sure what the correct default value is. Maybe just the empty string "" since the logic itself sits in Windows' own ShellExecuteW?

3. Missing arguments from >= 3.10

os.startfile gained additional arguments in Python 3.10, but they are all missing from the type annotations.

def startfile(
    filepath: StrOrBytesPath, 
    operation: str = "",  # Can't be None during runtime
    arguments: str = "",  # Can't be None during runtime
    cwd: StrOrBytesPath | None = None,  # Can be None during runtime
    show_cmd: int = 1  # Can't be None during runtime, 1 from CPython source
) -> None: ...

Disclaimer

I've been investigating this issue from my limited view point. There could be historic reasons why some things are the way they are. That's why I'm opening this as an Issue open for discussion instead of a PR. If you or somebody you know have more insight on this topic, please do share.

My proposal

If there are no mistakes in my assumptions, I would suggest the following annotations for os.startfile:

if sys.platform == "win32":
    if sys.version_info >= (3, 10):
        def startfile(filepath: StrOrBytesPath, operation: str = "", arguments: str = "", cwd: StrOrBytesPath | None = None, show_cmd: int = 1) -> None: ...
    else:
        def startfile(filepath: StrOrBytesPath, operation: str = '') -> None: ...

Activity

  1. srittau commented on Nov 7, 2023

    @srittau
    Collaborator

    I haven't looked in depth at the suggested changed, but a few notes: Changing filepath's name seems to have been an oversight. It was changed in #9815 using a script. Cc @AlexWaygood. For operation we can just use ... as default to signal that it's a complex/unknown default. Proper PR welcome!

  2. hauntsaninja commented on Nov 7, 2023

    @hauntsaninja
    Collaborator

    Technically it looks like #9815 fixed the argument name, it's just that it only fixed it on Python 3.7

  3. added a commit that references this issue on Nov 8, 2023
  4. JelleZijlstra commented on Nov 8, 2023

    @JelleZijlstra
    Member

    I was curious why stubtest didn't catch this, as the function has an Argument Clinic signature in CPython. It appears to be because

    lists "posix" under modules that don't exist on Windows. But apparently it does exist?

  5. AlexWaygood commented on Nov 8, 2023

    @AlexWaygood
    Member

    But apparently it does exist?

    not on my machine!

    Python 3.12.0 (tags/v3.12.0:0fb18b0, Oct  2 2023, 13:03:39) [MSC v.1935 64 bit (AMD64)] on win32
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import posix
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ModuleNotFoundError: No module named 'posix'

    Also, I believe that that allowlist entry should only allowlist posix itself not existing in the stubs. If you wanted to allowlist everything in posix as well as allowlisting the module itself, I think you'd need something like posix.* for an allowlist entry.

  6. AlexWaygood commented on Nov 8, 2023

    @AlexWaygood
    Member

    To be clear: on Windows, the posix module does "exist"... it's just it's called nt instead of posix on Windows, for Reasons. But the nt module shares the same source code as the posix module, it just goes by a different name on Windows builds.

  7. Jakob-Stadler commented on Nov 8, 2023

    @Jakob-Stadler
    ContributorAuthor

    From what I could gather during my research, posix becomes nt somewhere along the line.

  8. JelleZijlstra commented on Nov 8, 2023

    @JelleZijlstra
    Member

    Oops, sorry. Then I don't know why stubtest doesn't catch this. Does os.startfile.__text_signature__ exist?

  9. AlexWaygood commented on Nov 8, 2023

    @AlexWaygood
    Member
    >>> import os
    >>> os.startfile.__text_signature__
    '($module, /, filepath, operation=<unrepresentable>,\n          arguments=<unrepresentable>, cwd=None, show_cmd=1)'
    >>> import inspect
    >>> inspect.signature(os.startfile)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "C:\Users\alexw\AppData\Local\Programs\Python\Python312\Lib\inspect.py", line 3327, in signature
        return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "C:\Users\alexw\AppData\Local\Programs\Python\Python312\Lib\inspect.py", line 3071, in from_callable
        return _signature_from_callable(obj, sigcls=cls,
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "C:\Users\alexw\AppData\Local\Programs\Python\Python312\Lib\inspect.py", line 2564, in _signature_from_callable
        return _signature_from_builtin(sigcls, obj,
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "C:\Users\alexw\AppData\Local\Programs\Python\Python312\Lib\inspect.py", line 2365, in _signature_from_builtin
        return _signature_fromstr(cls, func, s, skip_bound_arg)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "C:\Users\alexw\AppData\Local\Programs\Python\Python312\Lib\inspect.py", line 2226, in _signature_fromstr
        raise ValueError("{!r} builtin has invalid signature".format(obj))
    ValueError: <built-in function startfile> builtin has invalid signature
  10. JelleZijlstra commented on Nov 8, 2023

    @JelleZijlstra
    Member

    I see, so we may have to teach something to deal with these "<unrepresentable>" defaults.

  11. JelleZijlstra commented on Nov 9, 2023

    @JelleZijlstra
    Member

    Cross-referencing python/cpython#87233

  12. added a commit that references this issue on Jan 26, 2024
    d6403b1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions