Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Providing basic advice on network related failures #5380

Open
pradyunsg opened this issue May 7, 2018 · 13 comments
Open

Providing basic advice on network related failures #5380

pradyunsg opened this issue May 7, 2018 · 13 comments
Labels
C: error messages Improving error messages state: needs discussion This needs some more discussion type: enhancement Improvements to functionality UX User experience related

Comments

@pradyunsg
Copy link
Member

Is your feature request related to a problem? Please describe.

Currently, many users of pip see weird error messages regarding network connectivity. While these messages are helpful for debugging the situation, they aren't exactly good UX.

Describe the solution you'd like

It might be nice if pip tried to cover some bases and suggested the user to look into common mistakes like not using a proxy when it's needed. Just an idea for discussion.

Additional context

@pypa/pip-committers Actually, I was wondering...

There's quite a few situations where we're presenting a Traceback today and we could improve the user experience by printing a message and the error and printing the Traceback only when verbose, like we did in #5239.

@pradyunsg pradyunsg added type: enhancement Improvements to functionality state: needs discussion This needs some more discussion labels May 7, 2018
@pradyunsg pradyunsg added this to the Print Better Error Messages milestone May 7, 2018
@pradyunsg
Copy link
Member Author

pradyunsg commented Sep 7, 2018

cmdargs: ['/Users/pradyunsg/Projects/pip/.tox/py36/bin/python', '/Users/pradyunsg/Projects/pip/tools/tox_pip.py', 'install', '-r/Users/pradyunsg/Projects/pip/tools/tests-requirements.txt']
Collecting freezegun (from -r /Users/pradyunsg/Projects/pip/tools/tests-requirements.txt (line 1))
  Using cached https://files.pythonhosted.org/packages/1b/78/feef0b235f1fed24aa5e617dee51f16d7cfd236bdacd0319718ce4706092/freezegun-0.3.10-py2.py3-none-any.whl
Collecting mock (from -r /Users/pradyunsg/Projects/pip/tools/tests-requirements.txt (line 2))
  Using cached https://files.pythonhosted.org/packages/e6/35/f187bdf23be87092bd0f1200d43d23076cee4d0dec109f195173fd3ebc79/mock-2.0.0-py2.py3-none-any.whl
Collecting pretend (from -r /Users/pradyunsg/Projects/pip/tools/tests-requirements.txt (line 3))
  Using cached https://files.pythonhosted.org/packages/49/1f/3d4f0579913edd3ad5b23ad52fcc42531cb736ad52af2ba6c057da8785b6/pretend-1.0.9-py2.py3-none-any.whl
Collecting pytest (from -r /Users/pradyunsg/Projects/pip/tools/tests-requirements.txt (line 4))
  Using cached https://files.pythonhosted.org/packages/dd/e5/1ce7de3e87ec499a056800fa0d7a689d6502d791c44eb1315a6ecadcb333/pytest-3.8.0-py2.py3-none-any.whl
Collecting pytest-cov (from -r /Users/pradyunsg/Projects/pip/tools/tests-requirements.txt (line 5))
  Using cached https://files.pythonhosted.org/packages/30/0a/1b009b525526cd3cd9f52f52391b426c5a3597447be811a10bcb1f6b05eb/pytest_cov-2.6.0-py2.py3-none-any.whl
Collecting pytest-rerunfailures (from -r /Users/pradyunsg/Projects/pip/tools/tests-requirements.txt (line 6))
  Using cached https://files.pythonhosted.org/packages/c5/81/2dc013556d53c9f9a1955e66f8620a05690eb6de9efc9fdb6cd748c66d24/pytest_rerunfailures-4.1-py2.py3-none-any.whl
Collecting pytest-timeout (from -r /Users/pradyunsg/Projects/pip/tools/tests-requirements.txt (line 7))
  Using cached https://files.pythonhosted.org/packages/ae/77/3b714fcfda89925be29f5cdea5b6199912265f54dc23b9af7d8c588e1830/pytest_timeout-1.3.2-py2.py3-none-any.whl
Collecting pytest-xdist (from -r /Users/pradyunsg/Projects/pip/tools/tests-requirements.txt (line 8))
  Using cached https://files.pythonhosted.org/packages/06/9f/d742d21f278a40c146362e69bc3d92d80b713368a4d0c48565b5c7820611/pytest_xdist-1.23.0-py2.py3-none-any.whl
Collecting pyyaml (from -r /Users/pradyunsg/Projects/pip/tools/tests-requirements.txt (line 9))
Collecting scripttest (from -r /Users/pradyunsg/Projects/pip/tools/tests-requirements.txt (line 10))
Collecting virtualenv from https://github.com/pypa/virtualenv/archive/master.zip#egg=virtualenv (from -r /Users/pradyunsg/Projects/pip/tools/tests-requirements.txt (line 11))
  Downloading https://github.com/pypa/virtualenv/archive/master.zip
Exception:
Traceback (most recent call last):
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_vendor/urllib3/response.py", line 331, in _error_catcher
    yield
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_vendor/urllib3/response.py", line 640, in read_chunked
    chunk = self._handle_chunk(amt)
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_vendor/urllib3/response.py", line 586, in _handle_chunk
    value = self._fp._safe_read(amt)
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_vendor/cachecontrol/filewrapper.py", line 70, in _safe_read
    data = self.__fp._safe_read(amt)
  File "/Users/pradyunsg/.pyenv/versions/3.6.5/lib/python3.6/http/client.py", line 612, in _safe_read
    chunk = self.fp.read(min(amt, MAXAMOUNT))
  File "/Users/pradyunsg/.pyenv/versions/3.6.5/lib/python3.6/socket.py", line 586, in readinto
    return self._sock.recv_into(b)
  File "/Users/pradyunsg/.pyenv/versions/3.6.5/lib/python3.6/ssl.py", line 1009, in recv_into
    return self.read(nbytes, buffer)
  File "/Users/pradyunsg/.pyenv/versions/3.6.5/lib/python3.6/ssl.py", line 871, in read
    return self._sslobj.read(len, buffer)
  File "/Users/pradyunsg/.pyenv/versions/3.6.5/lib/python3.6/ssl.py", line 631, in read
    v = self._sslobj.read(len, buffer)
socket.timeout: The read operation timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/basecommand.py", line 141, in main
    status = self.run(options, args)
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/commands/install.py", line 299, in run
    resolver.resolve(requirement_set)
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/resolve.py", line 102, in resolve
    self._resolve_one(requirement_set, req)
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/resolve.py", line 256, in _resolve_one
    abstract_dist = self._get_abstract_dist_for(req_to_install)
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/resolve.py", line 209, in _get_abstract_dist_for
    self.require_hashes
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/operations/prepare.py", line 283, in prepare_linked_requirement
    progress_bar=self.progress_bar
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/download.py", line 836, in unpack_url
    progress_bar=progress_bar
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/download.py", line 673, in unpack_http_url
    progress_bar)
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/download.py", line 897, in _download_http_url
    _download_url(resp, link, content_file, hashes, progress_bar)
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/download.py", line 619, in _download_url
    consume(downloaded_chunks)
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/utils/misc.py", line 844, in consume
    deque(iterator, maxlen=0)
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/download.py", line 585, in written_chunks
    for chunk in chunks:
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/utils/ui.py", line 159, in iter
    for x in it:
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_internal/download.py", line 574, in resp_read
    decode_content=False):
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_vendor/urllib3/response.py", line 461, in stream
    for line in self.read_chunked(amt, decode_content=decode_content):
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_vendor/urllib3/response.py", line 665, in read_chunked
    self._original_response.close()
  File "/Users/pradyunsg/.pyenv/versions/3.6.5/lib/python3.6/contextlib.py", line 99, in __exit__
    self.gen.throw(type, value, traceback)
  File "/Users/pradyunsg/Projects/pip/.tox/py36/lib/python3.6/site-packages/pip/_vendor/urllib3/response.py", line 336, in _error_catcher
    raise ReadTimeoutError(self._pool, None, 'Read timed out.')
pip._vendor.urllib3.exceptions.ReadTimeoutError: HTTPSConnectionPool(host='codeload.github.com', port=443): Read timed out.

Instead of the entire error traceback, just being able to show the relevant information would be useful.

@chrahunt
Copy link
Member

Another example from #4723:

NewConnectionError('<pip._vendor.requests.packages.urllib3.conne
ction.VerifiedHTTPSConnection object at 0x00000000035E1B70>: Failed to establish
a new connection: [Errno 11004] getaddrinfo failed',)': /simple/robotframework/

As mentioned in #1876, these kinds of messages could be better, e.g.

Failed to get address for <hostname>! Check your network connectivity and DNS settings. Details: [Errno 11004] getaddrinfo failed',)': /simple/robotframework/

@gutsytechster
Copy link
Contributor

Where could these errors be incorporated? On looking through the codebase, I think, this would be the place to do so- https://github.com/pypa/pip/blob/master/src/pip/_internal/network/download.py#L190?

@uranusjr
Copy link
Member

uranusjr commented Apr 23, 2020

There are two places that can raise exceptions. The place you mentioned would raise if the connection can’t be established (e.g. no Internet whatsoever). The other is pip._internal.network.utils.response_chunks(), where the response content is read out.

I think the best way to handle these is to introduce a new exception subclass, and raise it in these locations. For the former, I’d actually prefer we replace the raise_for_status() in _http_get_download() directly and avoid raising HTTPError altogether (for another reason #7486—this; in fact all raise_for_status() occurences could be replaced by this error message). The latter would involve catching errors from urllib3 and raise the exception subclass.

The new exception subclass should be handled in BaseCommand._main() along with InstallationError etc.

@gutsytechster
Copy link
Contributor

gutsytechster commented Apr 23, 2020

Thank you for the response @uranusjr. Could you clear these doubts?

  • When you say about the former, do you actually mean the error which would be raised when a connection can't be established? And for that, you prefer to replace all occurrences of raise_for_status() with the similar message mentioned here Providing basic advice on network related failures #5380 (comment). However, we would still require to raise the new exception subclass with that message, won't we?

  • And when you talk about the latter, do you meant the pip._internal.network.utils.response_chunks() piece of code, where the exceptions would be caught based on the urllib3 exceptions?

And just to know, why do we use urllib3 and requests separately. I mean, won't anyone of them would be sufficient to do the tasks of other one?

@uranusjr
Copy link
Member

uranusjr commented Apr 23, 2020

Yes to both your questions. I would be very happy if we replace all raise_for_status() occurrences, but this doesn’t need to be done all at once.

pip does not call urllib3 directly, only requests. But requests uses urllib3 internally, and sometimes just lets urllib3 errors bubble up to the call site without catching/re-raising them. I don’t want to deal with this either, but it’s just the world we live in :(

@gutsytechster
Copy link
Contributor

@uranusjr when you say to replace all occurrences of raise_for_status. AFAICT, the main aim here is not to raise HTTPError, rather a subclass of our own exception. But we still would need to evaluate the response just as the raise_for_status does, won't we? And for that, there would be a similar method implementation just as raise_for_status with the only difference in the final exception raised?

@uranusjr
Copy link
Member

uranusjr commented May 1, 2020

Yup.

@nlhkabu nlhkabu added C: error messages Improving error messages UX User experience related labels Jul 28, 2020
@nlhkabu nlhkabu removed this from the Print Better Error Messages milestone Jul 29, 2020
bors bot added a commit to duckinator/emanate that referenced this issue Jul 30, 2020
153: Update pip to 20.2 r=duckinator a=pyup-bot


This PR updates [pip](https://pypi.org/project/pip) from **20.1.1** to **20.2**.



<details>
  <summary>Changelog</summary>
  
  
   ### 20.2
   ```
   =================

Deprecations and Removals
-------------------------

- Deprecate setup.py-based builds that do not generate an ``.egg-info`` directory. (`6998 &lt;https://github.com/pypa/pip/issues/6998&gt;`_, `8617 &lt;https://github.com/pypa/pip/issues/8617&gt;`_)
- Disallow passing install-location-related arguments in ``--install-options``. (`7309 &lt;https://github.com/pypa/pip/issues/7309&gt;`_)
- Add deprecation warning for invalid requirements format &quot;base&gt;=1.0[extra]&quot; (`8288 &lt;https://github.com/pypa/pip/issues/8288&gt;`_)
- Deprecate legacy setup.py install when building a wheel failed for source
  distributions without pyproject.toml (`8368 &lt;https://github.com/pypa/pip/issues/8368&gt;`_)
- Deprecate -b/--build/--build-dir/--build-directory. Its current behaviour is confusing
  and breaks in case different versions of the same distribution need to be built during
  the resolution process. Using the TMPDIR/TEMP/TMP environment variable, possibly
  combined with --no-clean covers known use cases. (`8372 &lt;https://github.com/pypa/pip/issues/8372&gt;`_)
- Remove undocumented and deprecated option ``--always-unzip`` (`8408 &lt;https://github.com/pypa/pip/issues/8408&gt;`_)

Features
--------

- Log debugging information about pip, in ``pip install --verbose``. (`3166 &lt;https://github.com/pypa/pip/issues/3166&gt;`_)
- Refine error messages to avoid showing Python tracebacks when an HTTP error occurs. (`5380 &lt;https://github.com/pypa/pip/issues/5380&gt;`_)
- Install wheel files directly instead of extracting them to a temp directory. (`6030 &lt;https://github.com/pypa/pip/issues/6030&gt;`_)
- Add a beta version of pip&#39;s next-generation dependency resolver.

  Move pip&#39;s new resolver into beta, remove the
  ``--unstable-feature=resolver`` flag, and enable the
  ``--use-feature=2020-resolver`` flag. The new resolver is
  significantly stricter and more consistent when it receives
  incompatible instructions, and reduces support for certain kinds of
  :ref:`Constraints Files`, so some workarounds and workflows may
  break. More details about how to test and migrate, and how to report
  issues, at :ref:`Resolver changes 2020` . Maintainers are preparing to
   ```
   
  
  
   ### 20.2b1
   ```
   ===================

Bug Fixes
---------

- Correctly treat wheels containing non-ASCII file contents so they can be
  installed on Windows. (`5712 &lt;https://github.com/pypa/pip/issues/5712&gt;`_)
- Prompt the user for password if the keyring backend doesn&#39;t return one (`7998 &lt;https://github.com/pypa/pip/issues/7998&gt;`_)

Improved Documentation
----------------------

- Add GitHub issue template for reporting when the dependency resolver fails (`8207 &lt;https://github.com/pypa/pip/issues/8207&gt;`_)
   ```
   
  
</details>


 

<details>
  <summary>Links</summary>
  
  - PyPI: https://pypi.org/project/pip
  - Changelog: https://pyup.io/changelogs/pip/
  - Homepage: https://pip.pypa.io/
</details>



Co-authored-by: pyup-bot <github-bot@pyup.io>
bors bot added a commit to duckinator/emanate that referenced this issue Jul 31, 2020
153: Update pip to 20.2 r=duckinator a=pyup-bot


This PR updates [pip](https://pypi.org/project/pip) from **20.1.1** to **20.2**.



<details>
  <summary>Changelog</summary>
  
  
   ### 20.2
   ```
   =================

Deprecations and Removals
-------------------------

- Deprecate setup.py-based builds that do not generate an ``.egg-info`` directory. (`6998 &lt;https://github.com/pypa/pip/issues/6998&gt;`_, `8617 &lt;https://github.com/pypa/pip/issues/8617&gt;`_)
- Disallow passing install-location-related arguments in ``--install-options``. (`7309 &lt;https://github.com/pypa/pip/issues/7309&gt;`_)
- Add deprecation warning for invalid requirements format &quot;base&gt;=1.0[extra]&quot; (`8288 &lt;https://github.com/pypa/pip/issues/8288&gt;`_)
- Deprecate legacy setup.py install when building a wheel failed for source
  distributions without pyproject.toml (`8368 &lt;https://github.com/pypa/pip/issues/8368&gt;`_)
- Deprecate -b/--build/--build-dir/--build-directory. Its current behaviour is confusing
  and breaks in case different versions of the same distribution need to be built during
  the resolution process. Using the TMPDIR/TEMP/TMP environment variable, possibly
  combined with --no-clean covers known use cases. (`8372 &lt;https://github.com/pypa/pip/issues/8372&gt;`_)
- Remove undocumented and deprecated option ``--always-unzip`` (`8408 &lt;https://github.com/pypa/pip/issues/8408&gt;`_)

Features
--------

- Log debugging information about pip, in ``pip install --verbose``. (`3166 &lt;https://github.com/pypa/pip/issues/3166&gt;`_)
- Refine error messages to avoid showing Python tracebacks when an HTTP error occurs. (`5380 &lt;https://github.com/pypa/pip/issues/5380&gt;`_)
- Install wheel files directly instead of extracting them to a temp directory. (`6030 &lt;https://github.com/pypa/pip/issues/6030&gt;`_)
- Add a beta version of pip&#39;s next-generation dependency resolver.

  Move pip&#39;s new resolver into beta, remove the
  ``--unstable-feature=resolver`` flag, and enable the
  ``--use-feature=2020-resolver`` flag. The new resolver is
  significantly stricter and more consistent when it receives
  incompatible instructions, and reduces support for certain kinds of
  :ref:`Constraints Files`, so some workarounds and workflows may
  break. More details about how to test and migrate, and how to report
  issues, at :ref:`Resolver changes 2020` . Maintainers are preparing to
   ```
   
  
  
   ### 20.2b1
   ```
   ===================

Bug Fixes
---------

- Correctly treat wheels containing non-ASCII file contents so they can be
  installed on Windows. (`5712 &lt;https://github.com/pypa/pip/issues/5712&gt;`_)
- Prompt the user for password if the keyring backend doesn&#39;t return one (`7998 &lt;https://github.com/pypa/pip/issues/7998&gt;`_)

Improved Documentation
----------------------

- Add GitHub issue template for reporting when the dependency resolver fails (`8207 &lt;https://github.com/pypa/pip/issues/8207&gt;`_)
   ```
   
  
</details>


 

<details>
  <summary>Links</summary>
  
  - PyPI: https://pypi.org/project/pip
  - Changelog: https://pyup.io/changelogs/pip/
  - Homepage: https://pip.pypa.io/
</details>



Co-authored-by: pyup-bot <github-bot@pyup.io>
Co-authored-by: Ellen Marie Dash <me@duckie.co>
@zahlman
Copy link

zahlman commented Jun 10, 2024

A little disappointing to see this issue go nowhere for 6 years, because I have seen that users are quite seriously put off by this.

Here's a clear (anonymized) example of a poor message that I constantly see people report on Stack Overflow, on the Discourse and elsewhere:

WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ReadTimeoutError("HTTPSConnectionPool(host='files.pythonhosted.org', port=443): Read timed out. (read timeout=15)")': /path/to/some-package-1.0.0-py3-none-any.whl.metadata

Almost all of this information is useless to the user in context:

  • the user knows which package Pip is trying to install
  • I've literally never seen connect, read, redirect or status have any values other than None when an error like this is reported, and it wouldn't help understand what to do about the problem anyway
  • The classes Retry, ReadTimeoutError and HTTPSConnectionPool are implementation details in code that the user didn't write and isn't trying to call upon (only use, as an ordinary software user)
  • Some version of "read timeout" appears three times, and "retry" appears twice
  • The port number information isn't important unless there's an actual problem to solve (as opposed to an intermittent connection glitch)

The useful information is hard to extract and strangely labelled from the user's perspective:

  • The 4 for total=4 tells us how many further times Pip will re-try the connection
  • The server didn't respond to Pip, which is described as a "read timeout" because Pip set a time limit to read data from the server - that's standard terminology for people accustomed to writing networking code, but otherwise obscure
  • Pip spent 15 seconds waiting for the connection; that's the timeout value, and units are not mentioned
  • It's a metadata file, as indicated by the extension

And it's especially unpleasant to see all this text repeated five times (because retrying often doesn't solve the problem), culminating in this error (which essentially repeats it a sixth time):

ERROR: Could not install packages due to an OSError: HTTPSConnectionPool(host='files.pythonhosted.org', port=443): Max retries exceeded with url: /path/to/some-package-1.0.0-py3-none-any.whl.metadata (Caused by ReadTimeoutError("HTTPSConnectionPool(host='files.pythonhosted.org', port=443): Read timed out. (read timeout=15)"))

And none of this concretely suggests what might be done about it.

Here's a rough cut at how it should look instead:

WARNING: having difficulty connecting to files.pythonhosted.org via HTTPS (will try 4 more times). The server didn't respond within 15 seconds.
WARNING: having difficulty connecting to files.pythonhosted.org via HTTPS (will try 3 more times). The server didn't respond within 15 seconds.
WARNING: having difficulty connecting to files.pythonhosted.org via HTTPS (will try 2 more times). The server didn't respond within 15 seconds.
WARNING: having difficulty connecting to files.pythonhosted.org via HTTPS (will try 1 more time). The server didn't respond within 15 seconds.
WARNING: having difficulty connecting to files.pythonhosted.org via HTTPS (giving up). The server didn't respond within 15 seconds.
ERROR: Failed to install some-package==1.0.0: couldn't retrieve metadata because of a connection issue.
Please make sure that you are connected to the Internet and permitted to make outgoing connections to files.pythonhosted.org on port 443 (check your firewall and/or proxy settings) before trying again.

@pfmoore
Copy link
Member

pfmoore commented Jun 10, 2024

A little disappointing to see this issue go nowhere for 6 years, because I have seen that users are quite seriously put off by this.

The main reason is that no-one has offered a PR improving this. Pip is a volunteer driven project with limited maintainer resource, so we rely heavily on community contributions.

@zahlman
Copy link

zahlman commented Jun 10, 2024

Ah, I should realize that much.

Do the suggested messages look right, at least? It would be rather unpleasant to start searching around the codebase for how to change this, and then start working on a feature branch, only to find out that the change isn't actually wanted.

@pfmoore
Copy link
Member

pfmoore commented Jun 10, 2024

The proposed output certainly seems good.

@pradyunsg
Copy link
Member Author

x-ref urllib3/urllib3#2580 -- these messages don't originate out of pip.

I don't have bandwidth to argue against "go nowhere for 6 years", so I'll say that it's a bit disappointing for me too that this hasn't improved. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C: error messages Improving error messages state: needs discussion This needs some more discussion type: enhancement Improvements to functionality UX User experience related
Projects
None yet
Development

No branches or pull requests

7 participants