Hello,

I have a proposal to improve support of __main__.py (python -m foobar)
invocation to argparse.ArgumentParser

You can find attached a PEP draft.

Unfortunately, I'm not very confident on how to add that PEP and ...
honnestly not very used to github.

In short, can you help me and advise on this?

Thanks a lot and best regards,
Michaël Hooreman

PEP: 9999
Title: Add support of __main__.py to argparse.ArgumentParser.prog
Version: $Revision$
Last-Modified: $Date$
Author: Michaël Hooreman <mich...@hooreman.be>
Status: Draft
Type: Standards track
Content-Type: text/x-rst
Created: 23-Aug-2019
Python-Version: 3.7
Post-History: None

Introduction
============

The standard library's ``argparse.ArgumentParser`` uses by default
``sys.argv[0]`` for his ``prog`` property.

It means that when using the ``python -m foobar`` invocation, which calls under
the hood the ``foobar.__main__`` module, the ``prog`` will be ``__main__.py``,
giving as usage: ``usage: __main__.py [-h] ...``.

This PEP proposes an enhancement on the ``ArgumentParser`` to build more
accurate usage information in this case.

Implementation proposal
=======================

This paragraph gives a proposal of patch. A better implementation would
obviously impact the implementation of ``ArgumentParser.prog``.


.. code-block:: python

    import argparse

    class ArgumentParser(argparse.ArgumentParser):
        def __init__(self, *args, **kwargs):
            argparse.ArgumentParser.__init__(self, *args, **kwargs)
            self._fixProg()

        def _fixProg(self):
            """Fixes the prog if this is __main__.py

            Identify the module used by comparing the file of __main__.py
            to the sys.path.

            Implementation details:
            -----------------------
            The current directory, if found in the sys.path, is considered as a
            last ressort: if this is not the case, and if we run a package
            while being somewhere in his hierarchy, we will have a false
            result ... which won't work if our packages uses relative
            imports.

            Applies:
            --------
            It only impact self.prog if the initial self.prog is __main__.py.
            In the other cases, it returs immediately.

            Returns:
            --------
            Nothing

            Assertions:
            -----------
            - The file name of __main__ is ... __main__.py
            - -m option is used
            - A POSIX operating system is used (see limitations in the PEP)

            Other exceptions:
            -----------------
            - ValueError if the __main__ package cannot be identified
            """
            def gen():
                mainFile = pathlib.Path(
                    sys.modules['__main__'].__file__
                ).resolve()
                assert mainFile.name == self.prog, \
                    "Expecting {self.prog}, got {mainFile.name}"

                libdirs = sys.path
                pwdIsLib = os.getcwd() in libdirs
                libdirs = [d for d in sys.path if d != os.getcwd()]
                if pwdIsLib:
                    libdirs.append(os.getcwd())

                for libdir in libdirs:
                    libdir = pathlib.Path(libdir).resolve()
                    if str(mainFile).startswith(str(libdir)):
                        remaining = mainFile.relative_to(libdir)
                        remaining = remaining.parent  # to drop the __init__.py
                        while remaining.name:
                            yield remaining.name
                            remaining = remaining.parent
                        return

            if self.prog != '__main__.py':
                return

            elts = list(gen())[::-1]
            if not elts:
                raise ValueError("Cannot identify qualified __main__ package")

            # This block is not portable
            assert os.name == 'posix', "/proc used: only for POSIX"
            with open(f'/proc/{os.getpid()}/cmdline') as fh:
                ll = fh.read().split('\0')
            executable = ll[0]
            mIx = [i for i, x in enumerate(ll) if x == '-m']
            assert len(mIx) == 1, f"Cannot find position of -m in {ll}"
            mIx = mIx[0]
            pyOptns = ll[1:(mIx + 1)]

            self.prog = f"{executable} {' '.join(pyOptns)} {'.'.join(elts)}"

Advantages
==========

More accurate command line usage with ``argparse`` for ``python -m``
invocations.

Limitations
===========

We need to identify the way python was invoked in order to produce an usage
similar to what has been used by the caller. We found no way to do that using
standard library features, so that we are using the ``/proc`` file system.

It means that the current proposal is limited to POSIX operating systems.

Copyright
=========

This document has been placed in the public domain.


..
   Local Variables:
   mode: indented-text
   indent-tabs-mode: nil
   sentence-end-double-space: t
   fill-column: 70
   coding: utf-8
   End:
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/VEGHR5GACYW7M6RVNEC323L2V43WJIKP/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to