diff options
-rw-r--r-- | Doc/library/email.generator.rst | 4 | ||||
-rw-r--r-- | Doc/library/email.rst | 5 | ||||
-rw-r--r-- | Doc/library/nntplib.rst | 586 | ||||
-rw-r--r-- | Doc/library/superseded.rst | 1 | ||||
-rw-r--r-- | Doc/tools/.nitignore | 1 | ||||
-rw-r--r-- | Doc/whatsnew/2.0.rst | 2 | ||||
-rw-r--r-- | Doc/whatsnew/2.4.rst | 2 | ||||
-rw-r--r-- | Doc/whatsnew/2.7.rst | 2 | ||||
-rw-r--r-- | Doc/whatsnew/3.1.rst | 2 | ||||
-rw-r--r-- | Doc/whatsnew/3.11.rst | 2 | ||||
-rw-r--r-- | Doc/whatsnew/3.12.rst | 2 | ||||
-rw-r--r-- | Doc/whatsnew/3.13.rst | 5 | ||||
-rw-r--r-- | Doc/whatsnew/3.2.rst | 8 | ||||
-rw-r--r-- | Doc/whatsnew/3.3.rst | 2 | ||||
-rw-r--r-- | Doc/whatsnew/3.9.rst | 8 | ||||
-rw-r--r-- | Lib/nntplib.py | 1093 | ||||
-rw-r--r-- | Lib/test/ssltests.py | 2 | ||||
-rw-r--r-- | Lib/test/support/socket_helper.py | 6 | ||||
-rw-r--r-- | Lib/test/test_nntplib.py | 1646 | ||||
-rw-r--r-- | Makefile.pre.in | 2 | ||||
-rw-r--r-- | Misc/NEWS.d/3.9.0a1.rst | 2 | ||||
-rw-r--r-- | Misc/NEWS.d/3.9.0a3.rst | 6 | ||||
-rw-r--r-- | Misc/NEWS.d/3.9.0b1.rst | 4 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2023-05-24-22-22-03.gh-issue-104773.NwpjhZ.rst | 2 | ||||
-rw-r--r-- | Python/stdlib_module_names.h | 1 | ||||
-rwxr-xr-x | Tools/wasm/wasm_assets.py | 1 |
26 files changed, 33 insertions, 3364 deletions
diff --git a/Doc/library/email.generator.rst b/Doc/library/email.generator.rst index 34ad7b7..eb775b6 100644 --- a/Doc/library/email.generator.rst +++ b/Doc/library/email.generator.rst @@ -10,8 +10,8 @@ One of the most common tasks is to generate the flat (serialized) version of the email message represented by a message object structure. You will need to -do this if you want to send your message via :meth:`smtplib.SMTP.sendmail` or -the :mod:`nntplib` module, or print the message on the console. Taking a +do this if you want to send your message via :meth:`smtplib.SMTP.sendmail`, +or print the message on the console. Taking a message object structure and producing a serialized representation is the job of the generator classes. diff --git a/Doc/library/email.rst b/Doc/library/email.rst index 816fae9..3a60390 100644 --- a/Doc/library/email.rst +++ b/Doc/library/email.rst @@ -15,7 +15,7 @@ The :mod:`email` package is a library for managing email messages. It is specifically *not* designed to do any sending of email messages to SMTP (:rfc:`2821`), NNTP, or other servers; those are functions of modules such as -:mod:`smtplib` and :mod:`nntplib`. The :mod:`email` package attempts to be as +:mod:`smtplib`. The :mod:`email` package attempts to be as RFC-compliant as possible, supporting :rfc:`5322` and :rfc:`6532`, as well as such MIME-related RFCs as :rfc:`2045`, :rfc:`2046`, :rfc:`2047`, :rfc:`2183`, and :rfc:`2231`. @@ -141,9 +141,6 @@ Legacy API: Module :mod:`imaplib` IMAP (Internet Message Access Protocol) client - Module :mod:`nntplib` - NNTP (Net News Transport Protocol) client - Module :mod:`mailbox` Tools for creating, reading, and managing collections of messages on disk using a variety standard formats. diff --git a/Doc/library/nntplib.rst b/Doc/library/nntplib.rst deleted file mode 100644 index 143e4e0..0000000 --- a/Doc/library/nntplib.rst +++ /dev/null @@ -1,586 +0,0 @@ -:mod:`nntplib` --- NNTP protocol client -======================================= - -.. module:: nntplib - :synopsis: NNTP protocol client (requires sockets). - :deprecated: - -**Source code:** :source:`Lib/nntplib.py` - -.. index:: - pair: NNTP; protocol - single: Network News Transfer Protocol - -.. deprecated:: 3.11 - The :mod:`nntplib` module is deprecated (see :pep:`594` for details). - -.. testsetup:: - - import warnings - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=DeprecationWarning) - import nntplib - -.. testcleanup:: - - try: - s.quit() - except NameError: - pass - import sys - # Force a warning if any other file imports nntplib - sys.modules.pop('nntplib') - --------------- - -This module defines the class :class:`NNTP` which implements the client side of -the Network News Transfer Protocol. It can be used to implement a news reader -or poster, or automated news processors. It is compatible with :rfc:`3977` -as well as the older :rfc:`977` and :rfc:`2980`. - -.. include:: ../includes/wasm-notavail.rst - -Here are two small examples of how it can be used. To list some statistics -about a newsgroup and print the subjects of the last 10 articles:: - - >>> s = nntplib.NNTP('news.gmane.io') - >>> resp, count, first, last, name = s.group('gmane.comp.python.committers') - >>> print('Group', name, 'has', count, 'articles, range', first, 'to', last) - Group gmane.comp.python.committers has 1096 articles, range 1 to 1096 - >>> resp, overviews = s.over((last - 9, last)) - >>> for id, over in overviews: - ... print(id, nntplib.decode_header(over['subject'])) - ... - 1087 Re: Commit privileges for Łukasz Langa - 1088 Re: 3.2 alpha 2 freeze - 1089 Re: 3.2 alpha 2 freeze - 1090 Re: Commit privileges for Łukasz Langa - 1091 Re: Commit privileges for Łukasz Langa - 1092 Updated ssh key - 1093 Re: Updated ssh key - 1094 Re: Updated ssh key - 1095 Hello fellow committers! - 1096 Re: Hello fellow committers! - >>> s.quit() - '205 Bye!' - -To post an article from a binary file (this assumes that the article has valid -headers, and that you have right to post on the particular newsgroup):: - - >>> s = nntplib.NNTP('news.gmane.io') - >>> f = open('article.txt', 'rb') - >>> s.post(f) - '240 Article posted successfully.' - >>> s.quit() - '205 Bye!' - -The module itself defines the following classes: - - -.. class:: NNTP(host, port=119, user=None, password=None, readermode=None, usenetrc=False, [timeout]) - - Return a new :class:`NNTP` object, representing a connection - to the NNTP server running on host *host*, listening at port *port*. - An optional *timeout* can be specified for the socket connection. - If the optional *user* and *password* are provided, or if suitable - credentials are present in :file:`/.netrc` and the optional flag *usenetrc* - is true, the ``AUTHINFO USER`` and ``AUTHINFO PASS`` commands are used - to identify and authenticate the user to the server. If the optional - flag *readermode* is true, then a ``mode reader`` command is sent before - authentication is performed. Reader mode is sometimes necessary if you are - connecting to an NNTP server on the local machine and intend to call - reader-specific commands, such as ``group``. If you get unexpected - :exc:`NNTPPermanentError`\ s, you might need to set *readermode*. - The :class:`NNTP` class supports the :keyword:`with` statement to - unconditionally consume :exc:`OSError` exceptions and to close the NNTP - connection when done, e.g.: - - >>> from nntplib import NNTP - >>> with NNTP('news.gmane.io') as n: - ... n.group('gmane.comp.python.committers') - ... # doctest: +SKIP - ('211 1755 1 1755 gmane.comp.python.committers', 1755, 1, 1755, 'gmane.comp.python.committers') - >>> - - .. audit-event:: nntplib.connect self,host,port nntplib.NNTP - - .. audit-event:: nntplib.putline self,line nntplib.NNTP - - All commands will raise an :ref:`auditing event <auditing>` - ``nntplib.putline`` with arguments ``self`` and ``line``, - where ``line`` is the bytes about to be sent to the remote host. - - .. versionchanged:: 3.2 - *usenetrc* is now ``False`` by default. - - .. versionchanged:: 3.3 - Support for the :keyword:`with` statement was added. - - .. versionchanged:: 3.9 - If the *timeout* parameter is set to be zero, it will raise a - :class:`ValueError` to prevent the creation of a non-blocking socket. - -.. class:: NNTP_SSL(host, port=563, user=None, password=None, ssl_context=None, readermode=None, usenetrc=False, [timeout]) - - Return a new :class:`NNTP_SSL` object, representing an encrypted - connection to the NNTP server running on host *host*, listening at - port *port*. :class:`NNTP_SSL` objects have the same methods as - :class:`NNTP` objects. If *port* is omitted, port 563 (NNTPS) is used. - *ssl_context* is also optional, and is a :class:`~ssl.SSLContext` object. - Please read :ref:`ssl-security` for best practices. - All other parameters behave the same as for :class:`NNTP`. - - Note that SSL-on-563 is discouraged per :rfc:`4642`, in favor of - STARTTLS as described below. However, some servers only support the - former. - - .. audit-event:: nntplib.connect self,host,port nntplib.NNTP_SSL - - .. audit-event:: nntplib.putline self,line nntplib.NNTP_SSL - - All commands will raise an :ref:`auditing event <auditing>` - ``nntplib.putline`` with arguments ``self`` and ``line``, - where ``line`` is the bytes about to be sent to the remote host. - - .. versionadded:: 3.2 - - .. versionchanged:: 3.4 - The class now supports hostname check with - :attr:`ssl.SSLContext.check_hostname` and *Server Name Indication* (see - :data:`ssl.HAS_SNI`). - - .. versionchanged:: 3.9 - If the *timeout* parameter is set to be zero, it will raise a - :class:`ValueError` to prevent the creation of a non-blocking socket. - -.. exception:: NNTPError - - Derived from the standard exception :exc:`Exception`, this is the base - class for all exceptions raised by the :mod:`nntplib` module. Instances - of this class have the following attribute: - - .. attribute:: response - - The response of the server if available, as a :class:`str` object. - - -.. exception:: NNTPReplyError - - Exception raised when an unexpected reply is received from the server. - - -.. exception:: NNTPTemporaryError - - Exception raised when a response code in the range 400--499 is received. - - -.. exception:: NNTPPermanentError - - Exception raised when a response code in the range 500--599 is received. - - -.. exception:: NNTPProtocolError - - Exception raised when a reply is received from the server that does not begin - with a digit in the range 1--5. - - -.. exception:: NNTPDataError - - Exception raised when there is some error in the response data. - - -.. _nntp-objects: - -NNTP Objects ------------- - -When connected, :class:`NNTP` and :class:`NNTP_SSL` objects support the -following methods and attributes. - -Attributes -^^^^^^^^^^ - -.. attribute:: NNTP.nntp_version - - An integer representing the version of the NNTP protocol supported by the - server. In practice, this should be ``2`` for servers advertising - :rfc:`3977` compliance and ``1`` for others. - - .. versionadded:: 3.2 - -.. attribute:: NNTP.nntp_implementation - - A string describing the software name and version of the NNTP server, - or :const:`None` if not advertised by the server. - - .. versionadded:: 3.2 - -Methods -^^^^^^^ - -The *response* that is returned as the first item in the return tuple of almost -all methods is the server's response: a string beginning with a three-digit -code. If the server's response indicates an error, the method raises one of -the above exceptions. - -Many of the following methods take an optional keyword-only argument *file*. -When the *file* argument is supplied, it must be either a :term:`file object` -opened for binary writing, or the name of an on-disk file to be written to. -The method will then write any data returned by the server (except for the -response line and the terminating dot) to the file; any list of lines, -tuples or objects that the method normally returns will be empty. - -.. versionchanged:: 3.2 - Many of the following methods have been reworked and fixed, which makes - them incompatible with their 3.1 counterparts. - - -.. method:: NNTP.quit() - - Send a ``QUIT`` command and close the connection. Once this method has been - called, no other methods of the NNTP object should be called. - - -.. method:: NNTP.getwelcome() - - Return the welcome message sent by the server in reply to the initial - connection. (This message sometimes contains disclaimers or help information - that may be relevant to the user.) - - -.. method:: NNTP.getcapabilities() - - Return the :rfc:`3977` capabilities advertised by the server, as a - :class:`dict` instance mapping capability names to (possibly empty) lists - of values. On legacy servers which don't understand the ``CAPABILITIES`` - command, an empty dictionary is returned instead. - - >>> s = NNTP('news.gmane.io') - >>> 'POST' in s.getcapabilities() - True - - .. versionadded:: 3.2 - - -.. method:: NNTP.login(user=None, password=None, usenetrc=True) - - Send ``AUTHINFO`` commands with the user name and password. If *user* - and *password* are ``None`` and *usenetrc* is true, credentials from - ``~/.netrc`` will be used if possible. - - Unless intentionally delayed, login is normally performed during the - :class:`NNTP` object initialization and separately calling this function - is unnecessary. To force authentication to be delayed, you must not set - *user* or *password* when creating the object, and must set *usenetrc* to - False. - - .. versionadded:: 3.2 - - -.. method:: NNTP.starttls(context=None) - - Send a ``STARTTLS`` command. This will enable encryption on the NNTP - connection. The *context* argument is optional and should be a - :class:`ssl.SSLContext` object. Please read :ref:`ssl-security` for best - practices. - - Note that this may not be done after authentication information has - been transmitted, and authentication occurs by default if possible during a - :class:`NNTP` object initialization. See :meth:`NNTP.login` for information - on suppressing this behavior. - - .. versionadded:: 3.2 - - .. versionchanged:: 3.4 - The method now supports hostname check with - :attr:`ssl.SSLContext.check_hostname` and *Server Name Indication* (see - :data:`ssl.HAS_SNI`). - -.. method:: NNTP.newgroups(date, *, file=None) - - Send a ``NEWGROUPS`` command. The *date* argument should be a - :class:`datetime.date` or :class:`datetime.datetime` object. - Return a pair ``(response, groups)`` where *groups* is a list representing - the groups that are new since the given *date*. If *file* is supplied, - though, then *groups* will be empty. - - >>> from datetime import date, timedelta - >>> resp, groups = s.newgroups(date.today() - timedelta(days=3)) - >>> len(groups) # doctest: +SKIP - 85 - >>> groups[0] # doctest: +SKIP - GroupInfo(group='gmane.network.tor.devel', last='4', first='1', flag='m') - - -.. method:: NNTP.newnews(group, date, *, file=None) - - Send a ``NEWNEWS`` command. Here, *group* is a group name or ``'*'``, and - *date* has the same meaning as for :meth:`newgroups`. Return a pair - ``(response, articles)`` where *articles* is a list of message ids. - - This command is frequently disabled by NNTP server administrators. - - -.. method:: NNTP.list(group_pattern=None, *, file=None) - - Send a ``LIST`` or ``LIST ACTIVE`` command. Return a pair - ``(response, list)`` where *list* is a list of tuples representing all - the groups available from this NNTP server, optionally matching the - pattern string *group_pattern*. Each tuple has the form - ``(group, last, first, flag)``, where *group* is a group name, *last* - and *first* are the last and first article numbers, and *flag* usually - takes one of these values: - - * ``y``: Local postings and articles from peers are allowed. - * ``m``: The group is moderated and all postings must be approved. - * ``n``: No local postings are allowed, only articles from peers. - * ``j``: Articles from peers are filed in the junk group instead. - * ``x``: No local postings, and articles from peers are ignored. - * ``=foo.bar``: Articles are filed in the ``foo.bar`` group instead. - - If *flag* has another value, then the status of the newsgroup should be - considered unknown. - - This command can return very large results, especially if *group_pattern* - is not specified. It is best to cache the results offline unless you - really need to refresh them. - - .. versionchanged:: 3.2 - *group_pattern* was added. - - -.. method:: NNTP.descriptions(grouppattern) - - Send a ``LIST NEWSGROUPS`` command, where *grouppattern* is a wildmat string as - specified in :rfc:`3977` (it's essentially the same as DOS or UNIX shell wildcard - strings). Return a pair ``(response, descriptions)``, where *descriptions* - is a dictionary mapping group names to textual descriptions. - - >>> resp, descs = s.descriptions('gmane.comp.python.*') - >>> len(descs) # doctest: +SKIP - 295 - >>> descs.popitem() # doctest: +SKIP - ('gmane.comp.python.bio.general', 'BioPython discussion list (Moderated)') - - -.. method:: NNTP.description(group) - - Get a description for a single group *group*. If more than one group matches - (if 'group' is a real wildmat string), return the first match. If no group - matches, return an empty string. - - This elides the response code from the server. If the response code is needed, - use :meth:`descriptions`. - - -.. method:: NNTP.group(name) - - Send a ``GROUP`` command, where *name* is the group name. The group is - selected as the current group, if it exists. Return a tuple - ``(response, count, first, last, name)`` where *count* is the (estimated) - number of articles in the group, *first* is the first article number in - the group, *last* is the last article number in the group, and *name* - is the group name. - - -.. method:: NNTP.over(message_spec, *, file=None) - - Send an ``OVER`` command, or an ``XOVER`` command on legacy servers. - *message_spec* can be either a string representing a message id, or - a ``(first, last)`` tuple of numbers indicating a range of articles in - the current group, or a ``(first, None)`` tuple indicating a range of - articles starting from *first* to the last article in the current group, - or :const:`None` to select the current article in the current group. - - Return a pair ``(response, overviews)``. *overviews* is a list of - ``(article_number, overview)`` tuples, one for each article selected - by *message_spec*. Each *overview* is a dictionary with the same number - of items, but this number depends on the server. These items are either - message headers (the key is then the lower-cased header name) or metadata - items (the key is then the metadata name prepended with ``":"``). The - following items are guaranteed to be present by the NNTP specification: - - * the ``subject``, ``from``, ``date``, ``message-id`` and ``references`` - headers - * the ``:bytes`` metadata: the number of bytes in the entire raw article - (including headers and body) - * the ``:lines`` metadata: the number of lines in the article body - - The value of each item is either a string, or :const:`None` if not present. - - It is advisable to use the :func:`decode_header` function on header - values when they may contain non-ASCII characters:: - - >>> _, _, first, last, _ = s.group('gmane.comp.python.devel') - >>> resp, overviews = s.over((last, last)) - >>> art_num, over = overviews[0] - >>> art_num - 117216 - >>> list(over.keys()) - ['xref', 'from', ':lines', ':bytes', 'references', 'date', 'message-id', 'subject'] - >>> over['from'] - '=?UTF-8?B?Ik1hcnRpbiB2LiBMw7Z3aXMi?= <martin@v.loewis.de>' - >>> nntplib.decode_header(over['from']) - '"Martin v. Löwis" <martin@v.loewis.de>' - - .. versionadded:: 3.2 - - -.. method:: NNTP.help(*, file=None) - - Send a ``HELP`` command. Return a pair ``(response, list)`` where *list* is a - list of help strings. - - -.. method:: NNTP.stat(message_spec=None) - - Send a ``STAT`` command, where *message_spec* is either a message id - (enclosed in ``'<'`` and ``'>'``) or an article number in the current group. - If *message_spec* is omitted or :const:`None`, the current article in the - current group is considered. Return a triple ``(response, number, id)`` - where *number* is the article number and *id* is the message id. - - >>> _, _, first, last, _ = s.group('gmane.comp.python.devel') - >>> resp, number, message_id = s.stat(first) - >>> number, message_id - (9099, '<20030112190404.GE29873@epoch.metaslash.com>') - - -.. method:: NNTP.next() - - Send a ``NEXT`` command. Return as for :meth:`.stat`. - - -.. method:: NNTP.last() - - Send a ``LAST`` command. Return as for :meth:`.stat`. - - -.. method:: NNTP.article(message_spec=None, *, file=None) - - Send an ``ARTICLE`` command, where *message_spec* has the same meaning as - for :meth:`.stat`. Return a tuple ``(response, info)`` where *info* - is a :class:`~collections.namedtuple` with three attributes *number*, - *message_id* and *lines* (in that order). *number* is the article number - in the group (or 0 if the information is not available), *message_id* the - message id as a string, and *lines* a list of lines (without terminating - newlines) comprising the raw message including headers and body. - - >>> resp, info = s.article('<20030112190404.GE29873@epoch.metaslash.com>') - >>> info.number - 0 - >>> info.message_id - '<20030112190404.GE29873@epoch.metaslash.com>' - >>> len(info.lines) - 65 - >>> info.lines[0] - b'Path: main.gmane.org!not-for-mail' - >>> info.lines[1] - b'From: Neal Norwitz <neal@metaslash.com>' - >>> info.lines[-3:] - [b'There is a patch for 2.3 as well as 2.2.', b'', b'Neal'] - - -.. method:: NNTP.head(message_spec=None, *, file=None) - - Same as :meth:`article()`, but sends a ``HEAD`` command. The *lines* - returned (or written to *file*) will only contain the message headers, not - the body. - - -.. method:: NNTP.body(message_spec=None, *, file=None) - - Same as :meth:`article()`, but sends a ``BODY`` command. The *lines* - returned (or written to *file*) will only contain the message body, not the - headers. - - -.. method:: NNTP.post(data) - - Post an article using the ``POST`` command. The *data* argument is either - a :term:`file object` opened for binary reading, or any iterable of bytes - objects (representing raw lines of the article to be posted). It should - represent a well-formed news article, including the required headers. The - :meth:`post` method automatically escapes lines beginning with ``.`` and - appends the termination line. - - If the method succeeds, the server's response is returned. If the server - refuses posting, a :class:`NNTPReplyError` is raised. - - -.. method:: NNTP.ihave(message_id, data) - - Send an ``IHAVE`` command. *message_id* is the id of the message to send - to the server (enclosed in ``'<'`` and ``'>'``). The *data* parameter - and the return value are the same as for :meth:`post()`. - - -.. method:: NNTP.date() - - Return a pair ``(response, date)``. *date* is a :class:`~datetime.datetime` - object containing the current date and time of the server. - - -.. method:: NNTP.slave() - - Send a ``SLAVE`` command. Return the server's *response*. - - -.. method:: NNTP.set_debuglevel(level) - - Set the instance's debugging level. This controls the amount of debugging - output printed. The default, ``0``, produces no debugging output. A value of - ``1`` produces a moderate amount of debugging output, generally a single line - per request or response. A value of ``2`` or higher produces the maximum amount - of debugging output, logging each line sent and received on the connection - (including message text). - - -The following are optional NNTP extensions defined in :rfc:`2980`. Some of -them have been superseded by newer commands in :rfc:`3977`. - - -.. method:: NNTP.xhdr(hdr, str, *, file=None) - - Send an ``XHDR`` command. The *hdr* argument is a header keyword, e.g. - ``'subject'``. The *str* argument should have the form ``'first-last'`` - where *first* and *last* are the first and last article numbers to search. - Return a pair ``(response, list)``, where *list* is a list of pairs ``(id, - text)``, where *id* is an article number (as a string) and *text* is the text of - the requested header for that article. If the *file* parameter is supplied, then - the output of the ``XHDR`` command is stored in a file. If *file* is a string, - then the method will open a file with that name, write to it then close it. - If *file* is a :term:`file object`, then it will start calling :meth:`write` on - it to store the lines of the command output. If *file* is supplied, then the - returned *list* is an empty list. - - -.. method:: NNTP.xover(start, end, *, file=None) - - Send an ``XOVER`` command. *start* and *end* are article numbers - delimiting the range of articles to select. The return value is the - same of for :meth:`over()`. It is recommended to use :meth:`over()` - instead, since it will automatically use the newer ``OVER`` command - if available. - - -Utility functions ------------------ - -The module also defines the following utility function: - - -.. function:: decode_header(header_str) - - Decode a header value, un-escaping any escaped non-ASCII characters. - *header_str* must be a :class:`str` object. The unescaped value is - returned. Using this function is recommended to display some headers - in a human readable form:: - - >>> decode_header("Some subject") - 'Some subject' - >>> decode_header("=?ISO-8859-15?Q?D=E9buter_en_Python?=") - 'Débuter en Python' - >>> decode_header("Re: =?UTF-8?B?cHJvYmzDqG1lIGRlIG1hdHJpY2U=?=") - 'Re: problème de matrice' diff --git a/Doc/library/superseded.rst b/Doc/library/superseded.rst index 7440738..1e60420 100644 --- a/Doc/library/superseded.rst +++ b/Doc/library/superseded.rst @@ -17,7 +17,6 @@ backwards compatibility. They have been superseded by other modules. imghdr.rst msilib.rst nis.rst - nntplib.rst optparse.rst uu.rst xdrlib.rst diff --git a/Doc/tools/.nitignore b/Doc/tools/.nitignore index cdd896e..7043589 100644 --- a/Doc/tools/.nitignore +++ b/Doc/tools/.nitignore @@ -166,7 +166,6 @@ Doc/library/msvcrt.rst Doc/library/multiprocessing.rst Doc/library/multiprocessing.shared_memory.rst Doc/library/netrc.rst -Doc/library/nntplib.rst Doc/library/numbers.rst Doc/library/operator.rst Doc/library/optparse.rst diff --git a/Doc/whatsnew/2.0.rst b/Doc/whatsnew/2.0.rst index 76094d4..e6c7ec9 100644 --- a/Doc/whatsnew/2.0.rst +++ b/Doc/whatsnew/2.0.rst @@ -1032,7 +1032,7 @@ Lots of improvements and bugfixes were made to Python's extensive standard library; some of the affected modules include :mod:`readline`, :mod:`ConfigParser`, :mod:`!cgi`, :mod:`calendar`, :mod:`posix`, :mod:`readline`, :mod:`xmllib`, :mod:`aifc`, :mod:`chunk, wave`, :mod:`random`, :mod:`shelve`, -and :mod:`nntplib`. Consult the CVS logs for the exact patch-by-patch details. +and :mod:`!nntplib`. Consult the CVS logs for the exact patch-by-patch details. Brian Gallew contributed OpenSSL support for the :mod:`socket` module. OpenSSL is an implementation of the Secure Socket Layer, which encrypts the data being diff --git a/Doc/whatsnew/2.4.rst b/Doc/whatsnew/2.4.rst index 98dc83f..43c3f01 100644 --- a/Doc/whatsnew/2.4.rst +++ b/Doc/whatsnew/2.4.rst @@ -1191,7 +1191,7 @@ complete list of changes, or look through the CVS logs for all the details. effect is to make :file:`.pyc` files significantly smaller. (Contributed by Martin von Löwis.) -* The :mod:`nntplib` module's :class:`NNTP` class gained :meth:`description` and +* The :mod:`!nntplib` module's :class:`NNTP` class gained :meth:`description` and :meth:`descriptions` methods to retrieve newsgroup descriptions for a single group or for a range of groups. (Contributed by Jürgen A. Erhard.) diff --git a/Doc/whatsnew/2.7.rst b/Doc/whatsnew/2.7.rst index 36afcb1..f8c7872 100644 --- a/Doc/whatsnew/2.7.rst +++ b/Doc/whatsnew/2.7.rst @@ -1429,7 +1429,7 @@ changes, or look through the Subversion logs for all the details. become very large. (Contributed by Charles Cazabon; :issue:`6963`.) -* The :mod:`nntplib` module now supports IPv6 addresses. +* The :mod:`!nntplib` module now supports IPv6 addresses. (Contributed by Derek Morr; :issue:`1664`.) * New functions: the :mod:`os` module wraps the following POSIX system diff --git a/Doc/whatsnew/3.1.rst b/Doc/whatsnew/3.1.rst index fba8816..054762d 100644 --- a/Doc/whatsnew/3.1.rst +++ b/Doc/whatsnew/3.1.rst @@ -377,7 +377,7 @@ New, Improved, and Deprecated Modules (Contributed by Ross Light; :issue:`4285`.) -* The :mod:`nntplib` and :mod:`imaplib` modules now support IPv6. +* The :mod:`!nntplib` and :mod:`imaplib` modules now support IPv6. (Contributed by Derek Morr; :issue:`1655` and :issue:`1664`.) diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index af60052..37463b6 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -1735,7 +1735,7 @@ Modules +---------------------+---------------------+---------------------+---------------------+---------------------+ | :mod:`audioop` | :mod:`crypt` | :mod:`nis` | :mod:`!sndhdr` | :mod:`uu` | +---------------------+---------------------+---------------------+---------------------+---------------------+ - | :mod:`!cgi` | :mod:`imghdr` | :mod:`nntplib` | :mod:`!spwd` | :mod:`xdrlib` | + | :mod:`!cgi` | :mod:`imghdr` | :mod:`!nntplib` | :mod:`!spwd` | :mod:`xdrlib` | +---------------------+---------------------+---------------------+---------------------+---------------------+ | :mod:`!cgitb` | :mod:`!mailcap` | :mod:`!ossaudiodev` | :mod:`!sunau` | | +---------------------+---------------------+---------------------+---------------------+---------------------+ diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 3de778c..0a047fa 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -896,7 +896,7 @@ Modules (see :pep:`594`): * :mod:`!mailcap` * :mod:`msilib` * :mod:`nis` -* :mod:`nntplib` +* :mod:`!nntplib` * :mod:`!ossaudiodev` * :mod:`!pipes` * :mod:`!sndhdr` diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index a94a69a..e681a9d 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -173,6 +173,11 @@ Removed instead. (Contributed by Victor Stinner in :gh:`104773`.) +* :pep:`594`: Remove the :mod:`!nntplib` module, deprecated in Python 3.11: + the `PyPI nntplib project <https://pypi.org/project/nntplib/>`_ can be used + instead. + (Contributed by Victor Stinner in :gh:`104773`.) + Porting to Python 3.13 ====================== diff --git a/Doc/whatsnew/3.2.rst b/Doc/whatsnew/3.2.rst index 7af0c02..c3f7ef6 100644 --- a/Doc/whatsnew/3.2.rst +++ b/Doc/whatsnew/3.2.rst @@ -662,7 +662,7 @@ Python's standard library has undergone significant maintenance efforts and quality improvements. The biggest news for Python 3.2 is that the :mod:`email` package, :mod:`mailbox` -module, and :mod:`nntplib` modules now work correctly with the bytes/text model +module, and :mod:`!nntplib` modules now work correctly with the bytes/text model in Python 3. For the first time, there is correct handling of messages with mixed encodings. @@ -1676,13 +1676,13 @@ for secure (encrypted, authenticated) internet connections: nntp ---- -The :mod:`nntplib` module has a revamped implementation with better bytes and +The :mod:`!nntplib` module has a revamped implementation with better bytes and text semantics as well as more practical APIs. These improvements break compatibility with the nntplib version in Python 3.1, which was partly dysfunctional in itself. Support for secure connections through both implicit (using -:class:`nntplib.NNTP_SSL`) and explicit (using :meth:`nntplib.NNTP.starttls`) +:class:`!nntplib.NNTP_SSL`) and explicit (using :meth:`!nntplib.NNTP.starttls`) TLS has also been added. (Contributed by Antoine Pitrou in :issue:`9360` and Andrew Vant in :issue:`1926`.) @@ -2643,7 +2643,7 @@ require changes to your code: * ``""`` is now a valid value and is no longer automatically converted to an empty string. For empty strings, use ``"option ="`` in a line. -* The :mod:`nntplib` module was reworked extensively, meaning that its APIs +* The :mod:`!nntplib` module was reworked extensively, meaning that its APIs are often incompatible with the 3.1 APIs. * :class:`bytearray` objects can no longer be used as filenames; instead, diff --git a/Doc/whatsnew/3.3.rst b/Doc/whatsnew/3.3.rst index c05b8e5..b25cf36 100644 --- a/Doc/whatsnew/3.3.rst +++ b/Doc/whatsnew/3.3.rst @@ -1558,7 +1558,7 @@ Schlawack in :issue:`12708`.) nntplib ------- -The :class:`nntplib.NNTP` class now supports the context management protocol to +The :class:`!nntplib.NNTP` class now supports the context management protocol to unconditionally consume :exc:`socket.error` exceptions and to close the NNTP connection when done:: diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 54f5662..a1a4bf3 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -586,7 +586,7 @@ queue. nntplib ------- -:class:`~nntplib.NNTP` and :class:`~nntplib.NNTP_SSL` now raise a :class:`ValueError` +:class:`~!nntplib.NNTP` and :class:`~!nntplib.NNTP_SSL` now raise a :class:`ValueError` if the given timeout for their constructor is zero to prevent the creation of a non-blocking socket. (Contributed by Dong-hee Na in :issue:`39259`.) @@ -956,11 +956,11 @@ Removed * The erroneous version at :data:`unittest.mock.__version__` has been removed. -* :class:`nntplib.NNTP`: ``xpath()`` and ``xgtitle()`` methods have been removed. +* :class:`!nntplib.NNTP`: ``xpath()`` and ``xgtitle()`` methods have been removed. These methods are deprecated since Python 3.3. Generally, these extensions are not supported or not enabled by NNTP server administrators. - For ``xgtitle()``, please use :meth:`nntplib.NNTP.descriptions` or - :meth:`nntplib.NNTP.description` instead. + For ``xgtitle()``, please use :meth:`!nntplib.NNTP.descriptions` or + :meth:`!nntplib.NNTP.description` instead. (Contributed by Dong-hee Na in :issue:`39366`.) * :class:`array.array`: ``tostring()`` and ``fromstring()`` methods have been diff --git a/Lib/nntplib.py b/Lib/nntplib.py deleted file mode 100644 index dddea05..0000000 --- a/Lib/nntplib.py +++ /dev/null @@ -1,1093 +0,0 @@ -"""An NNTP client class based on: -- RFC 977: Network News Transfer Protocol -- RFC 2980: Common NNTP Extensions -- RFC 3977: Network News Transfer Protocol (version 2) - -Example: - ->>> from nntplib import NNTP ->>> s = NNTP('news') ->>> resp, count, first, last, name = s.group('comp.lang.python') ->>> print('Group', name, 'has', count, 'articles, range', first, 'to', last) -Group comp.lang.python has 51 articles, range 5770 to 5821 ->>> resp, subs = s.xhdr('subject', '{0}-{1}'.format(first, last)) ->>> resp = s.quit() ->>> - -Here 'resp' is the server response line. -Error responses are turned into exceptions. - -To post an article from a file: ->>> f = open(filename, 'rb') # file containing article, including header ->>> resp = s.post(f) ->>> - -For descriptions of all methods, read the comments in the code below. -Note that all arguments and return values representing article numbers -are strings, not numbers, since they are rarely used for calculations. -""" - -# RFC 977 by Brian Kantor and Phil Lapsley. -# xover, xgtitle, xpath, date methods by Kevan Heydon - -# Incompatible changes from the 2.x nntplib: -# - all commands are encoded as UTF-8 data (using the "surrogateescape" -# error handler), except for raw message data (POST, IHAVE) -# - all responses are decoded as UTF-8 data (using the "surrogateescape" -# error handler), except for raw message data (ARTICLE, HEAD, BODY) -# - the `file` argument to various methods is keyword-only -# -# - NNTP.date() returns a datetime object -# - NNTP.newgroups() and NNTP.newnews() take a datetime (or date) object, -# rather than a pair of (date, time) strings. -# - NNTP.newgroups() and NNTP.list() return a list of GroupInfo named tuples -# - NNTP.descriptions() returns a dict mapping group names to descriptions -# - NNTP.xover() returns a list of dicts mapping field names (header or metadata) -# to field values; each dict representing a message overview. -# - NNTP.article(), NNTP.head() and NNTP.body() return a (response, ArticleInfo) -# tuple. -# - the "internal" methods have been marked private (they now start with -# an underscore) - -# Other changes from the 2.x/3.1 nntplib: -# - automatic querying of capabilities at connect -# - New method NNTP.getcapabilities() -# - New method NNTP.over() -# - New helper function decode_header() -# - NNTP.post() and NNTP.ihave() accept file objects, bytes-like objects and -# arbitrary iterables yielding lines. -# - An extensive test suite :-) - -# TODO: -# - return structured data (GroupInfo etc.) everywhere -# - support HDR - -# Imports -import re -import socket -import collections -import datetime -import sys -import warnings - -try: - import ssl -except ImportError: - _have_ssl = False -else: - _have_ssl = True - -from email.header import decode_header as _email_decode_header -from socket import _GLOBAL_DEFAULT_TIMEOUT - -__all__ = ["NNTP", - "NNTPError", "NNTPReplyError", "NNTPTemporaryError", - "NNTPPermanentError", "NNTPProtocolError", "NNTPDataError", - "decode_header", - ] - -warnings._deprecated(__name__, remove=(3, 13)) - -# maximal line length when calling readline(). This is to prevent -# reading arbitrary length lines. RFC 3977 limits NNTP line length to -# 512 characters, including CRLF. We have selected 2048 just to be on -# the safe side. -_MAXLINE = 2048 - - -# Exceptions raised when an error or invalid response is received -class NNTPError(Exception): - """Base class for all nntplib exceptions""" - def __init__(self, *args): - Exception.__init__(self, *args) - try: - self.response = args[0] - except IndexError: - self.response = 'No response given' - -class NNTPReplyError(NNTPError): - """Unexpected [123]xx reply""" - pass - -class NNTPTemporaryError(NNTPError): - """4xx errors""" - pass - -class NNTPPermanentError(NNTPError): - """5xx errors""" - pass - -class NNTPProtocolError(NNTPError): - """Response does not begin with [1-5]""" - pass - -class NNTPDataError(NNTPError): - """Error in response data""" - pass - - -# Standard port used by NNTP servers -NNTP_PORT = 119 -NNTP_SSL_PORT = 563 - -# Response numbers that are followed by additional text (e.g. article) -_LONGRESP = { - '100', # HELP - '101', # CAPABILITIES - '211', # LISTGROUP (also not multi-line with GROUP) - '215', # LIST - '220', # ARTICLE - '221', # HEAD, XHDR - '222', # BODY - '224', # OVER, XOVER - '225', # HDR - '230', # NEWNEWS - '231', # NEWGROUPS - '282', # XGTITLE -} - -# Default decoded value for LIST OVERVIEW.FMT if not supported -_DEFAULT_OVERVIEW_FMT = [ - "subject", "from", "date", "message-id", "references", ":bytes", ":lines"] - -# Alternative names allowed in LIST OVERVIEW.FMT response -_OVERVIEW_FMT_ALTERNATIVES = { - 'bytes': ':bytes', - 'lines': ':lines', -} - -# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF) -_CRLF = b'\r\n' - -GroupInfo = collections.namedtuple('GroupInfo', - ['group', 'last', 'first', 'flag']) - -ArticleInfo = collections.namedtuple('ArticleInfo', - ['number', 'message_id', 'lines']) - - -# Helper function(s) -def decode_header(header_str): - """Takes a unicode string representing a munged header value - and decodes it as a (possibly non-ASCII) readable value.""" - parts = [] - for v, enc in _email_decode_header(header_str): - if isinstance(v, bytes): - parts.append(v.decode(enc or 'ascii')) - else: - parts.append(v) - return ''.join(parts) - -def _parse_overview_fmt(lines): - """Parse a list of string representing the response to LIST OVERVIEW.FMT - and return a list of header/metadata names. - Raises NNTPDataError if the response is not compliant - (cf. RFC 3977, section 8.4).""" - fmt = [] - for line in lines: - if line[0] == ':': - # Metadata name (e.g. ":bytes") - name, _, suffix = line[1:].partition(':') - name = ':' + name - else: - # Header name (e.g. "Subject:" or "Xref:full") - name, _, suffix = line.partition(':') - name = name.lower() - name = _OVERVIEW_FMT_ALTERNATIVES.get(name, name) - # Should we do something with the suffix? - fmt.append(name) - defaults = _DEFAULT_OVERVIEW_FMT - if len(fmt) < len(defaults): - raise NNTPDataError("LIST OVERVIEW.FMT response too short") - if fmt[:len(defaults)] != defaults: - raise NNTPDataError("LIST OVERVIEW.FMT redefines default fields") - return fmt - -def _parse_overview(lines, fmt, data_process_func=None): - """Parse the response to an OVER or XOVER command according to the - overview format `fmt`.""" - n_defaults = len(_DEFAULT_OVERVIEW_FMT) - overview = [] - for line in lines: - fields = {} - article_number, *tokens = line.split('\t') - article_number = int(article_number) - for i, token in enumerate(tokens): - if i >= len(fmt): - # XXX should we raise an error? Some servers might not - # support LIST OVERVIEW.FMT and still return additional - # headers. - continue - field_name = fmt[i] - is_metadata = field_name.startswith(':') - if i >= n_defaults and not is_metadata: - # Non-default header names are included in full in the response - # (unless the field is totally empty) - h = field_name + ": " - if token and token[:len(h)].lower() != h: - raise NNTPDataError("OVER/XOVER response doesn't include " - "names of additional headers") - token = token[len(h):] if token else None - fields[fmt[i]] = token - overview.append((article_number, fields)) - return overview - -def _parse_datetime(date_str, time_str=None): - """Parse a pair of (date, time) strings, and return a datetime object. - If only the date is given, it is assumed to be date and time - concatenated together (e.g. response to the DATE command). - """ - if time_str is None: - time_str = date_str[-6:] - date_str = date_str[:-6] - hours = int(time_str[:2]) - minutes = int(time_str[2:4]) - seconds = int(time_str[4:]) - year = int(date_str[:-4]) - month = int(date_str[-4:-2]) - day = int(date_str[-2:]) - # RFC 3977 doesn't say how to interpret 2-char years. Assume that - # there are no dates before 1970 on Usenet. - if year < 70: - year += 2000 - elif year < 100: - year += 1900 - return datetime.datetime(year, month, day, hours, minutes, seconds) - -def _unparse_datetime(dt, legacy=False): - """Format a date or datetime object as a pair of (date, time) strings - in the format required by the NEWNEWS and NEWGROUPS commands. If a - date object is passed, the time is assumed to be midnight (00h00). - - The returned representation depends on the legacy flag: - * if legacy is False (the default): - date has the YYYYMMDD format and time the HHMMSS format - * if legacy is True: - date has the YYMMDD format and time the HHMMSS format. - RFC 3977 compliant servers should understand both formats; therefore, - legacy is only needed when talking to old servers. - """ - if not isinstance(dt, datetime.datetime): - time_str = "000000" - else: - time_str = "{0.hour:02d}{0.minute:02d}{0.second:02d}".format(dt) - y = dt.year - if legacy: - y = y % 100 - date_str = "{0:02d}{1.month:02d}{1.day:02d}".format(y, dt) - else: - date_str = "{0:04d}{1.month:02d}{1.day:02d}".format(y, dt) - return date_str, time_str - - -if _have_ssl: - - def _encrypt_on(sock, context, hostname): - """Wrap a socket in SSL/TLS. Arguments: - - sock: Socket to wrap - - context: SSL context to use for the encrypted connection - Returns: - - sock: New, encrypted socket. - """ - # Generate a default SSL context if none was passed. - if context is None: - context = ssl._create_stdlib_context() - return context.wrap_socket(sock, server_hostname=hostname) - - -# The classes themselves -class NNTP: - # UTF-8 is the character set for all NNTP commands and responses: they - # are automatically encoded (when sending) and decoded (and receiving) - # by this class. - # However, some multi-line data blocks can contain arbitrary bytes (for - # example, latin-1 or utf-16 data in the body of a message). Commands - # taking (POST, IHAVE) or returning (HEAD, BODY, ARTICLE) raw message - # data will therefore only accept and produce bytes objects. - # Furthermore, since there could be non-compliant servers out there, - # we use 'surrogateescape' as the error handler for fault tolerance - # and easy round-tripping. This could be useful for some applications - # (e.g. NNTP gateways). - - encoding = 'utf-8' - errors = 'surrogateescape' - - def __init__(self, host, port=NNTP_PORT, user=None, password=None, - readermode=None, usenetrc=False, - timeout=_GLOBAL_DEFAULT_TIMEOUT): - """Initialize an instance. Arguments: - - host: hostname to connect to - - port: port to connect to (default the standard NNTP port) - - user: username to authenticate with - - password: password to use with username - - readermode: if true, send 'mode reader' command after - connecting. - - usenetrc: allow loading username and password from ~/.netrc file - if not specified explicitly - - timeout: timeout (in seconds) used for socket connections - - readermode is sometimes necessary if you are connecting to an - NNTP server on the local machine and intend to call - reader-specific commands, such as `group'. If you get - unexpected NNTPPermanentErrors, you might need to set - readermode. - """ - self.host = host - self.port = port - self.sock = self._create_socket(timeout) - self.file = None - try: - self.file = self.sock.makefile("rwb") - self._base_init(readermode) - if user or usenetrc: - self.login(user, password, usenetrc) - except: - if self.file: - self.file.close() - self.sock.close() - raise - - def _base_init(self, readermode): - """Partial initialization for the NNTP protocol. - This instance method is extracted for supporting the test code. - """ - self.debugging = 0 - self.welcome = self._getresp() - - # Inquire about capabilities (RFC 3977). - self._caps = None - self.getcapabilities() - - # 'MODE READER' is sometimes necessary to enable 'reader' mode. - # However, the order in which 'MODE READER' and 'AUTHINFO' need to - # arrive differs between some NNTP servers. If _setreadermode() fails - # with an authorization failed error, it will set this to True; - # the login() routine will interpret that as a request to try again - # after performing its normal function. - # Enable only if we're not already in READER mode anyway. - self.readermode_afterauth = False - if readermode and 'READER' not in self._caps: - self._setreadermode() - if not self.readermode_afterauth: - # Capabilities might have changed after MODE READER - self._caps = None - self.getcapabilities() - - # RFC 4642 2.2.2: Both the client and the server MUST know if there is - # a TLS session active. A client MUST NOT attempt to start a TLS - # session if a TLS session is already active. - self.tls_on = False - - # Log in and encryption setup order is left to subclasses. - self.authenticated = False - - def __enter__(self): - return self - - def __exit__(self, *args): - is_connected = lambda: hasattr(self, "file") - if is_connected(): - try: - self.quit() - except (OSError, EOFError): - pass - finally: - if is_connected(): - self._close() - - def _create_socket(self, timeout): - if timeout is not None and not timeout: - raise ValueError('Non-blocking socket (timeout=0) is not supported') - sys.audit("nntplib.connect", self, self.host, self.port) - return socket.create_connection((self.host, self.port), timeout) - - def getwelcome(self): - """Get the welcome message from the server - (this is read and squirreled away by __init__()). - If the response code is 200, posting is allowed; - if it 201, posting is not allowed.""" - - if self.debugging: print('*welcome*', repr(self.welcome)) - return self.welcome - - def getcapabilities(self): - """Get the server capabilities, as read by __init__(). - If the CAPABILITIES command is not supported, an empty dict is - returned.""" - if self._caps is None: - self.nntp_version = 1 - self.nntp_implementation = None - try: - resp, caps = self.capabilities() - except (NNTPPermanentError, NNTPTemporaryError): - # Server doesn't support capabilities - self._caps = {} - else: - self._caps = caps - if 'VERSION' in caps: - # The server can advertise several supported versions, - # choose the highest. - self.nntp_version = max(map(int, caps['VERSION'])) - if 'IMPLEMENTATION' in caps: - self.nntp_implementation = ' '.join(caps['IMPLEMENTATION']) - return self._caps - - def set_debuglevel(self, level): - """Set the debugging level. Argument 'level' means: - 0: no debugging output (default) - 1: print commands and responses but not body text etc. - 2: also print raw lines read and sent before stripping CR/LF""" - - self.debugging = level - debug = set_debuglevel - - def _putline(self, line): - """Internal: send one line to the server, appending CRLF. - The `line` must be a bytes-like object.""" - sys.audit("nntplib.putline", self, line) - line = line + _CRLF - if self.debugging > 1: print('*put*', repr(line)) - self.file.write(line) - self.file.flush() - - def _putcmd(self, line): - """Internal: send one command to the server (through _putline()). - The `line` must be a unicode string.""" - if self.debugging: print('*cmd*', repr(line)) - line = line.encode(self.encoding, self.errors) - self._putline(line) - - def _getline(self, strip_crlf=True): - """Internal: return one line from the server, stripping _CRLF. - Raise EOFError if the connection is closed. - Returns a bytes object.""" - line = self.file.readline(_MAXLINE +1) - if len(line) > _MAXLINE: - raise NNTPDataError('line too long') - if self.debugging > 1: - print('*get*', repr(line)) - if not line: raise EOFError - if strip_crlf: - if line[-2:] == _CRLF: - line = line[:-2] - elif line[-1:] in _CRLF: - line = line[:-1] - return line - - def _getresp(self): - """Internal: get a response from the server. - Raise various errors if the response indicates an error. - Returns a unicode string.""" - resp = self._getline() - if self.debugging: print('*resp*', repr(resp)) - resp = resp.decode(self.encoding, self.errors) - c = resp[:1] - if c == '4': - raise NNTPTemporaryError(resp) - if c == '5': - raise NNTPPermanentError(resp) - if c not in '123': - raise NNTPProtocolError(resp) - return resp - - def _getlongresp(self, file=None): - """Internal: get a response plus following text from the server. - Raise various errors if the response indicates an error. - - Returns a (response, lines) tuple where `response` is a unicode - string and `lines` is a list of bytes objects. - If `file` is a file-like object, it must be open in binary mode. - """ - - openedFile = None - try: - # If a string was passed then open a file with that name - if isinstance(file, (str, bytes)): - openedFile = file = open(file, "wb") - - resp = self._getresp() - if resp[:3] not in _LONGRESP: - raise NNTPReplyError(resp) - - lines = [] - if file is not None: - # XXX lines = None instead? - terminators = (b'.' + _CRLF, b'.\n') - while 1: - line = self._getline(False) - if line in terminators: - break - if line.startswith(b'..'): - line = line[1:] - file.write(line) - else: - terminator = b'.' - while 1: - line = self._getline() - if line == terminator: - break - if line.startswith(b'..'): - line = line[1:] - lines.append(line) - finally: - # If this method created the file, then it must close it - if openedFile: - openedFile.close() - - return resp, lines - - def _shortcmd(self, line): - """Internal: send a command and get the response. - Same return value as _getresp().""" - self._putcmd(line) - return self._getresp() - - def _longcmd(self, line, file=None): - """Internal: send a command and get the response plus following text. - Same return value as _getlongresp().""" - self._putcmd(line) - return self._getlongresp(file) - - def _longcmdstring(self, line, file=None): - """Internal: send a command and get the response plus following text. - Same as _longcmd() and _getlongresp(), except that the returned `lines` - are unicode strings rather than bytes objects. - """ - self._putcmd(line) - resp, list = self._getlongresp(file) - return resp, [line.decode(self.encoding, self.errors) - for line in list] - - def _getoverviewfmt(self): - """Internal: get the overview format. Queries the server if not - already done, else returns the cached value.""" - try: - return self._cachedoverviewfmt - except AttributeError: - pass - try: - resp, lines = self._longcmdstring("LIST OVERVIEW.FMT") - except NNTPPermanentError: - # Not supported by server? - fmt = _DEFAULT_OVERVIEW_FMT[:] - else: - fmt = _parse_overview_fmt(lines) - self._cachedoverviewfmt = fmt - return fmt - - def _grouplist(self, lines): - # Parse lines into "group last first flag" - return [GroupInfo(*line.split()) for line in lines] - - def capabilities(self): - """Process a CAPABILITIES command. Not supported by all servers. - Return: - - resp: server response if successful - - caps: a dictionary mapping capability names to lists of tokens - (for example {'VERSION': ['2'], 'OVER': [], LIST: ['ACTIVE', 'HEADERS'] }) - """ - caps = {} - resp, lines = self._longcmdstring("CAPABILITIES") - for line in lines: - name, *tokens = line.split() - caps[name] = tokens - return resp, caps - - def newgroups(self, date, *, file=None): - """Process a NEWGROUPS command. Arguments: - - date: a date or datetime object - Return: - - resp: server response if successful - - list: list of newsgroup names - """ - if not isinstance(date, (datetime.date, datetime.date)): - raise TypeError( - "the date parameter must be a date or datetime object, " - "not '{:40}'".format(date.__class__.__name__)) - date_str, time_str = _unparse_datetime(date, self.nntp_version < 2) - cmd = 'NEWGROUPS {0} {1}'.format(date_str, time_str) - resp, lines = self._longcmdstring(cmd, file) - return resp, self._grouplist(lines) - - def newnews(self, group, date, *, file=None): - """Process a NEWNEWS command. Arguments: - - group: group name or '*' - - date: a date or datetime object - Return: - - resp: server response if successful - - list: list of message ids - """ - if not isinstance(date, (datetime.date, datetime.date)): - raise TypeError( - "the date parameter must be a date or datetime object, " - "not '{:40}'".format(date.__class__.__name__)) - date_str, time_str = _unparse_datetime(date, self.nntp_version < 2) - cmd = 'NEWNEWS {0} {1} {2}'.format(group, date_str, time_str) - return self._longcmdstring(cmd, file) - - def list(self, group_pattern=None, *, file=None): - """Process a LIST or LIST ACTIVE command. Arguments: - - group_pattern: a pattern indicating which groups to query - - file: Filename string or file object to store the result in - Returns: - - resp: server response if successful - - list: list of (group, last, first, flag) (strings) - """ - if group_pattern is not None: - command = 'LIST ACTIVE ' + group_pattern - else: - command = 'LIST' - resp, lines = self._longcmdstring(command, file) - return resp, self._grouplist(lines) - - def _getdescriptions(self, group_pattern, return_all): - line_pat = re.compile('^(?P<group>[^ \t]+)[ \t]+(.*)$') - # Try the more std (acc. to RFC2980) LIST NEWSGROUPS first - resp, lines = self._longcmdstring('LIST NEWSGROUPS ' + group_pattern) - if not resp.startswith('215'): - # Now the deprecated XGTITLE. This either raises an error - # or succeeds with the same output structure as LIST - # NEWSGROUPS. - resp, lines = self._longcmdstring('XGTITLE ' + group_pattern) - groups = {} - for raw_line in lines: - match = line_pat.search(raw_line.strip()) - if match: - name, desc = match.group(1, 2) - if not return_all: - return desc - groups[name] = desc - if return_all: - return resp, groups - else: - # Nothing found - return '' - - def description(self, group): - """Get a description for a single group. If more than one - group matches ('group' is a pattern), return the first. If no - group matches, return an empty string. - - This elides the response code from the server, since it can - only be '215' or '285' (for xgtitle) anyway. If the response - code is needed, use the 'descriptions' method. - - NOTE: This neither checks for a wildcard in 'group' nor does - it check whether the group actually exists.""" - return self._getdescriptions(group, False) - - def descriptions(self, group_pattern): - """Get descriptions for a range of groups.""" - return self._getdescriptions(group_pattern, True) - - def group(self, name): - """Process a GROUP command. Argument: - - group: the group name - Returns: - - resp: server response if successful - - count: number of articles - - first: first article number - - last: last article number - - name: the group name - """ - resp = self._shortcmd('GROUP ' + name) - if not resp.startswith('211'): - raise NNTPReplyError(resp) - words = resp.split() - count = first = last = 0 - n = len(words) - if n > 1: - count = words[1] - if n > 2: - first = words[2] - if n > 3: - last = words[3] - if n > 4: - name = words[4].lower() - return resp, int(count), int(first), int(last), name - - def help(self, *, file=None): - """Process a HELP command. Argument: - - file: Filename string or file object to store the result in - Returns: - - resp: server response if successful - - list: list of strings returned by the server in response to the - HELP command - """ - return self._longcmdstring('HELP', file) - - def _statparse(self, resp): - """Internal: parse the response line of a STAT, NEXT, LAST, - ARTICLE, HEAD or BODY command.""" - if not resp.startswith('22'): - raise NNTPReplyError(resp) - words = resp.split() - art_num = int(words[1]) - message_id = words[2] - return resp, art_num, message_id - - def _statcmd(self, line): - """Internal: process a STAT, NEXT or LAST command.""" - resp = self._shortcmd(line) - return self._statparse(resp) - - def stat(self, message_spec=None): - """Process a STAT command. Argument: - - message_spec: article number or message id (if not specified, - the current article is selected) - Returns: - - resp: server response if successful - - art_num: the article number - - message_id: the message id - """ - if message_spec: - return self._statcmd('STAT {0}'.format(message_spec)) - else: - return self._statcmd('STAT') - - def next(self): - """Process a NEXT command. No arguments. Return as for STAT.""" - return self._statcmd('NEXT') - - def last(self): - """Process a LAST command. No arguments. Return as for STAT.""" - return self._statcmd('LAST') - - def _artcmd(self, line, file=None): - """Internal: process a HEAD, BODY or ARTICLE command.""" - resp, lines = self._longcmd(line, file) - resp, art_num, message_id = self._statparse(resp) - return resp, ArticleInfo(art_num, message_id, lines) - - def head(self, message_spec=None, *, file=None): - """Process a HEAD command. Argument: - - message_spec: article number or message id - - file: filename string or file object to store the headers in - Returns: - - resp: server response if successful - - ArticleInfo: (article number, message id, list of header lines) - """ - if message_spec is not None: - cmd = 'HEAD {0}'.format(message_spec) - else: - cmd = 'HEAD' - return self._artcmd(cmd, file) - - def body(self, message_spec=None, *, file=None): - """Process a BODY command. Argument: - - message_spec: article number or message id - - file: filename string or file object to store the body in - Returns: - - resp: server response if successful - - ArticleInfo: (article number, message id, list of body lines) - """ - if message_spec is not None: - cmd = 'BODY {0}'.format(message_spec) - else: - cmd = 'BODY' - return self._artcmd(cmd, file) - - def article(self, message_spec=None, *, file=None): - """Process an ARTICLE command. Argument: - - message_spec: article number or message id - - file: filename string or file object to store the article in - Returns: - - resp: server response if successful - - ArticleInfo: (article number, message id, list of article lines) - """ - if message_spec is not None: - cmd = 'ARTICLE {0}'.format(message_spec) - else: - cmd = 'ARTICLE' - return self._artcmd(cmd, file) - - def slave(self): - """Process a SLAVE command. Returns: - - resp: server response if successful - """ - return self._shortcmd('SLAVE') - - def xhdr(self, hdr, str, *, file=None): - """Process an XHDR command (optional server extension). Arguments: - - hdr: the header type (e.g. 'subject') - - str: an article nr, a message id, or a range nr1-nr2 - - file: Filename string or file object to store the result in - Returns: - - resp: server response if successful - - list: list of (nr, value) strings - """ - pat = re.compile('^([0-9]+) ?(.*)\n?') - resp, lines = self._longcmdstring('XHDR {0} {1}'.format(hdr, str), file) - def remove_number(line): - m = pat.match(line) - return m.group(1, 2) if m else line - return resp, [remove_number(line) for line in lines] - - def xover(self, start, end, *, file=None): - """Process an XOVER command (optional server extension) Arguments: - - start: start of range - - end: end of range - - file: Filename string or file object to store the result in - Returns: - - resp: server response if successful - - list: list of dicts containing the response fields - """ - resp, lines = self._longcmdstring('XOVER {0}-{1}'.format(start, end), - file) - fmt = self._getoverviewfmt() - return resp, _parse_overview(lines, fmt) - - def over(self, message_spec, *, file=None): - """Process an OVER command. If the command isn't supported, fall - back to XOVER. Arguments: - - message_spec: - - either a message id, indicating the article to fetch - information about - - or a (start, end) tuple, indicating a range of article numbers; - if end is None, information up to the newest message will be - retrieved - - or None, indicating the current article number must be used - - file: Filename string or file object to store the result in - Returns: - - resp: server response if successful - - list: list of dicts containing the response fields - - NOTE: the "message id" form isn't supported by XOVER - """ - cmd = 'OVER' if 'OVER' in self._caps else 'XOVER' - if isinstance(message_spec, (tuple, list)): - start, end = message_spec - cmd += ' {0}-{1}'.format(start, end or '') - elif message_spec is not None: - cmd = cmd + ' ' + message_spec - resp, lines = self._longcmdstring(cmd, file) - fmt = self._getoverviewfmt() - return resp, _parse_overview(lines, fmt) - - def date(self): - """Process the DATE command. - Returns: - - resp: server response if successful - - date: datetime object - """ - resp = self._shortcmd("DATE") - if not resp.startswith('111'): - raise NNTPReplyError(resp) - elem = resp.split() - if len(elem) != 2: - raise NNTPDataError(resp) - date = elem[1] - if len(date) != 14: - raise NNTPDataError(resp) - return resp, _parse_datetime(date, None) - - def _post(self, command, f): - resp = self._shortcmd(command) - # Raises a specific exception if posting is not allowed - if not resp.startswith('3'): - raise NNTPReplyError(resp) - if isinstance(f, (bytes, bytearray)): - f = f.splitlines() - # We don't use _putline() because: - # - we don't want additional CRLF if the file or iterable is already - # in the right format - # - we don't want a spurious flush() after each line is written - for line in f: - if not line.endswith(_CRLF): - line = line.rstrip(b"\r\n") + _CRLF - if line.startswith(b'.'): - line = b'.' + line - self.file.write(line) - self.file.write(b".\r\n") - self.file.flush() - return self._getresp() - - def post(self, data): - """Process a POST command. Arguments: - - data: bytes object, iterable or file containing the article - Returns: - - resp: server response if successful""" - return self._post('POST', data) - - def ihave(self, message_id, data): - """Process an IHAVE command. Arguments: - - message_id: message-id of the article - - data: file containing the article - Returns: - - resp: server response if successful - Note that if the server refuses the article an exception is raised.""" - return self._post('IHAVE {0}'.format(message_id), data) - - def _close(self): - try: - if self.file: - self.file.close() - del self.file - finally: - self.sock.close() - - def quit(self): - """Process a QUIT command and close the socket. Returns: - - resp: server response if successful""" - try: - resp = self._shortcmd('QUIT') - finally: - self._close() - return resp - - def login(self, user=None, password=None, usenetrc=True): - if self.authenticated: - raise ValueError("Already logged in.") - if not user and not usenetrc: - raise ValueError( - "At least one of `user` and `usenetrc` must be specified") - # If no login/password was specified but netrc was requested, - # try to get them from ~/.netrc - # Presume that if .netrc has an entry, NNRP authentication is required. - try: - if usenetrc and not user: - import netrc - credentials = netrc.netrc() - auth = credentials.authenticators(self.host) - if auth: - user = auth[0] - password = auth[2] - except OSError: - pass - # Perform NNTP authentication if needed. - if not user: - return - resp = self._shortcmd('authinfo user ' + user) - if resp.startswith('381'): - if not password: - raise NNTPReplyError(resp) - else: - resp = self._shortcmd('authinfo pass ' + password) - if not resp.startswith('281'): - raise NNTPPermanentError(resp) - # Capabilities might have changed after login - self._caps = None - self.getcapabilities() - # Attempt to send mode reader if it was requested after login. - # Only do so if we're not in reader mode already. - if self.readermode_afterauth and 'READER' not in self._caps: - self._setreadermode() - # Capabilities might have changed after MODE READER - self._caps = None - self.getcapabilities() - - def _setreadermode(self): - try: - self.welcome = self._shortcmd('mode reader') - except NNTPPermanentError: - # Error 5xx, probably 'not implemented' - pass - except NNTPTemporaryError as e: - if e.response.startswith('480'): - # Need authorization before 'mode reader' - self.readermode_afterauth = True - else: - raise - - if _have_ssl: - def starttls(self, context=None): - """Process a STARTTLS command. Arguments: - - context: SSL context to use for the encrypted connection - """ - # Per RFC 4642, STARTTLS MUST NOT be sent after authentication or if - # a TLS session already exists. - if self.tls_on: - raise ValueError("TLS is already enabled.") - if self.authenticated: - raise ValueError("TLS cannot be started after authentication.") - resp = self._shortcmd('STARTTLS') - if resp.startswith('382'): - self.file.close() - self.sock = _encrypt_on(self.sock, context, self.host) - self.file = self.sock.makefile("rwb") - self.tls_on = True - # Capabilities may change after TLS starts up, so ask for them - # again. - self._caps = None - self.getcapabilities() - else: - raise NNTPError("TLS failed to start.") - - -if _have_ssl: - class NNTP_SSL(NNTP): - - def __init__(self, host, port=NNTP_SSL_PORT, - user=None, password=None, ssl_context=None, - readermode=None, usenetrc=False, - timeout=_GLOBAL_DEFAULT_TIMEOUT): - """This works identically to NNTP.__init__, except for the change - in default port and the `ssl_context` argument for SSL connections. - """ - self.ssl_context = ssl_context - super().__init__(host, port, user, password, readermode, - usenetrc, timeout) - - def _create_socket(self, timeout): - sock = super()._create_socket(timeout) - try: - sock = _encrypt_on(sock, self.ssl_context, self.host) - except: - sock.close() - raise - else: - return sock - - __all__.append("NNTP_SSL") - - -# Test retrieval when run as a script. -if __name__ == '__main__': - import argparse - - parser = argparse.ArgumentParser(description="""\ - nntplib built-in demo - display the latest articles in a newsgroup""") - parser.add_argument('-g', '--group', default='gmane.comp.python.general', - help='group to fetch messages from (default: %(default)s)') - parser.add_argument('-s', '--server', default='news.gmane.io', - help='NNTP server hostname (default: %(default)s)') - parser.add_argument('-p', '--port', default=-1, type=int, - help='NNTP port number (default: %s / %s)' % (NNTP_PORT, NNTP_SSL_PORT)) - parser.add_argument('-n', '--nb-articles', default=10, type=int, - help='number of articles to fetch (default: %(default)s)') - parser.add_argument('-S', '--ssl', action='store_true', default=False, - help='use NNTP over SSL') - args = parser.parse_args() - - port = args.port - if not args.ssl: - if port == -1: - port = NNTP_PORT - s = NNTP(host=args.server, port=port) - else: - if port == -1: - port = NNTP_SSL_PORT - s = NNTP_SSL(host=args.server, port=port) - - caps = s.getcapabilities() - if 'STARTTLS' in caps: - s.starttls() - resp, count, first, last, name = s.group(args.group) - print('Group', name, 'has', count, 'articles, range', first, 'to', last) - - def cut(s, lim): - if len(s) > lim: - s = s[:lim - 4] + "..." - return s - - first = str(int(last) - args.nb_articles + 1) - resp, overviews = s.xover(first, last) - for artnum, over in overviews: - author = decode_header(over['from']).split('<', 1)[0] - subject = decode_header(over['subject']) - lines = int(over[':lines']) - print("{:7} {:20} {:42} ({})".format( - artnum, cut(author, 20), cut(subject, 42), lines) - ) - - s.quit() diff --git a/Lib/test/ssltests.py b/Lib/test/ssltests.py index 5073ae1..ee03aed 100644 --- a/Lib/test/ssltests.py +++ b/Lib/test/ssltests.py @@ -7,7 +7,7 @@ import subprocess TESTS = [ 'test_asyncio', 'test_ensurepip.py', 'test_ftplib', 'test_hashlib', - 'test_hmac', 'test_httplib', 'test_imaplib', 'test_nntplib', + 'test_hmac', 'test_httplib', 'test_imaplib', 'test_poplib', 'test_ssl', 'test_smtplib', 'test_smtpnet', 'test_urllib2_localnet', 'test_venv', 'test_xmlrpc' ] diff --git a/Lib/test/support/socket_helper.py b/Lib/test/support/socket_helper.py index 7840923..45f6d65 100644 --- a/Lib/test/support/socket_helper.py +++ b/Lib/test/support/socket_helper.py @@ -7,7 +7,6 @@ import tempfile import unittest from .. import support -from . import warnings_helper HOST = "localhost" HOSTv4 = "127.0.0.1" @@ -195,7 +194,6 @@ _NOT_SET = object() def transient_internet(resource_name, *, timeout=_NOT_SET, errnos=()): """Return a context manager that raises ResourceDenied when various issues with the internet connection manifest themselves as exceptions.""" - nntplib = warnings_helper.import_deprecated("nntplib") import urllib.error if timeout is _NOT_SET: timeout = support.INTERNET_TIMEOUT @@ -248,10 +246,6 @@ def transient_internet(resource_name, *, timeout=_NOT_SET, errnos=()): if timeout is not None: socket.setdefaulttimeout(timeout) yield - except nntplib.NNTPTemporaryError as err: - if support.verbose: - sys.stderr.write(denied.args[0] + "\n") - raise denied from err except OSError as err: # urllib can wrap original socket errors multiple times (!), we must # unwrap to get at the original error. diff --git a/Lib/test/test_nntplib.py b/Lib/test/test_nntplib.py deleted file mode 100644 index 31a02f8..0000000 --- a/Lib/test/test_nntplib.py +++ /dev/null @@ -1,1646 +0,0 @@ -import io -import socket -import datetime -import textwrap -import unittest -import functools -import contextlib -import os.path -import re -import threading - -from test import support -from test.support import socket_helper, warnings_helper -nntplib = warnings_helper.import_deprecated("nntplib") -from nntplib import NNTP, GroupInfo -from unittest.mock import patch -try: - import ssl -except ImportError: - ssl = None - - -certfile = os.path.join(os.path.dirname(__file__), 'keycert3.pem') - -if ssl is not None: - SSLError = ssl.SSLError -else: - class SSLError(Exception): - """Non-existent exception class when we lack SSL support.""" - reason = "This will never be raised." - -# TODO: -# - test the `file` arg to more commands -# - test error conditions -# - test auth and `usenetrc` - - -class NetworkedNNTPTestsMixin: - - ssl_context = None - - def test_welcome(self): - welcome = self.server.getwelcome() - self.assertEqual(str, type(welcome)) - - def test_help(self): - resp, lines = self.server.help() - self.assertTrue(resp.startswith("100 "), resp) - for line in lines: - self.assertEqual(str, type(line)) - - def test_list(self): - resp, groups = self.server.list() - if len(groups) > 0: - self.assertEqual(GroupInfo, type(groups[0])) - self.assertEqual(str, type(groups[0].group)) - - def test_list_active(self): - resp, groups = self.server.list(self.GROUP_PAT) - if len(groups) > 0: - self.assertEqual(GroupInfo, type(groups[0])) - self.assertEqual(str, type(groups[0].group)) - - def test_unknown_command(self): - with self.assertRaises(nntplib.NNTPPermanentError) as cm: - self.server._shortcmd("XYZZY") - resp = cm.exception.response - self.assertTrue(resp.startswith("500 "), resp) - - def test_newgroups(self): - # gmane gets a constant influx of new groups. In order not to stress - # the server too much, we choose a recent date in the past. - dt = datetime.date.today() - datetime.timedelta(days=7) - resp, groups = self.server.newgroups(dt) - if len(groups) > 0: - self.assertIsInstance(groups[0], GroupInfo) - self.assertIsInstance(groups[0].group, str) - - def test_description(self): - def _check_desc(desc): - # Sanity checks - self.assertIsInstance(desc, str) - self.assertNotIn(self.GROUP_NAME, desc) - desc = self.server.description(self.GROUP_NAME) - _check_desc(desc) - # Another sanity check - self.assertIn(self.DESC, desc) - # With a pattern - desc = self.server.description(self.GROUP_PAT) - _check_desc(desc) - # Shouldn't exist - desc = self.server.description("zk.brrtt.baz") - self.assertEqual(desc, '') - - def test_descriptions(self): - resp, descs = self.server.descriptions(self.GROUP_PAT) - # 215 for LIST NEWSGROUPS, 282 for XGTITLE - self.assertTrue( - resp.startswith("215 ") or resp.startswith("282 "), resp) - self.assertIsInstance(descs, dict) - desc = descs[self.GROUP_NAME] - self.assertEqual(desc, self.server.description(self.GROUP_NAME)) - - def test_group(self): - result = self.server.group(self.GROUP_NAME) - self.assertEqual(5, len(result)) - resp, count, first, last, group = result - self.assertEqual(group, self.GROUP_NAME) - self.assertIsInstance(count, int) - self.assertIsInstance(first, int) - self.assertIsInstance(last, int) - self.assertLessEqual(first, last) - self.assertTrue(resp.startswith("211 "), resp) - - def test_date(self): - resp, date = self.server.date() - self.assertIsInstance(date, datetime.datetime) - # Sanity check - self.assertGreaterEqual(date.year, 1995) - self.assertLessEqual(date.year, 2030) - - def _check_art_dict(self, art_dict): - # Some sanity checks for a field dictionary returned by OVER / XOVER - self.assertIsInstance(art_dict, dict) - # NNTP has 7 mandatory fields - self.assertGreaterEqual(art_dict.keys(), - {"subject", "from", "date", "message-id", - "references", ":bytes", ":lines"} - ) - for v in art_dict.values(): - self.assertIsInstance(v, (str, type(None))) - - def test_xover(self): - resp, count, first, last, name = self.server.group(self.GROUP_NAME) - resp, lines = self.server.xover(last - 5, last) - if len(lines) == 0: - self.skipTest("no articles retrieved") - # The 'last' article is not necessarily part of the output (cancelled?) - art_num, art_dict = lines[0] - self.assertGreaterEqual(art_num, last - 5) - self.assertLessEqual(art_num, last) - self._check_art_dict(art_dict) - - @unittest.skipIf(True, 'temporarily skipped until a permanent solution' - ' is found for issue #28971') - def test_over(self): - resp, count, first, last, name = self.server.group(self.GROUP_NAME) - start = last - 10 - # The "start-" article range form - resp, lines = self.server.over((start, None)) - art_num, art_dict = lines[0] - self._check_art_dict(art_dict) - # The "start-end" article range form - resp, lines = self.server.over((start, last)) - art_num, art_dict = lines[-1] - # The 'last' article is not necessarily part of the output (cancelled?) - self.assertGreaterEqual(art_num, start) - self.assertLessEqual(art_num, last) - self._check_art_dict(art_dict) - # XXX The "message_id" form is unsupported by gmane - # 503 Overview by message-ID unsupported - - def test_xhdr(self): - resp, count, first, last, name = self.server.group(self.GROUP_NAME) - resp, lines = self.server.xhdr('subject', last) - for line in lines: - self.assertEqual(str, type(line[1])) - - def check_article_resp(self, resp, article, art_num=None): - self.assertIsInstance(article, nntplib.ArticleInfo) - if art_num is not None: - self.assertEqual(article.number, art_num) - for line in article.lines: - self.assertIsInstance(line, bytes) - # XXX this could exceptionally happen... - self.assertNotIn(article.lines[-1], (b".", b".\n", b".\r\n")) - - @unittest.skipIf(True, "FIXME: see bpo-32128") - def test_article_head_body(self): - resp, count, first, last, name = self.server.group(self.GROUP_NAME) - # Try to find an available article - for art_num in (last, first, last - 1): - try: - resp, head = self.server.head(art_num) - except nntplib.NNTPTemporaryError as e: - if not e.response.startswith("423 "): - raise - # "423 No such article" => choose another one - continue - break - else: - self.skipTest("could not find a suitable article number") - self.assertTrue(resp.startswith("221 "), resp) - self.check_article_resp(resp, head, art_num) - resp, body = self.server.body(art_num) - self.assertTrue(resp.startswith("222 "), resp) - self.check_article_resp(resp, body, art_num) - resp, article = self.server.article(art_num) - self.assertTrue(resp.startswith("220 "), resp) - self.check_article_resp(resp, article, art_num) - # Tolerate running the tests from behind a NNTP virus checker - denylist = lambda line: line.startswith(b'X-Antivirus') - filtered_head_lines = [line for line in head.lines - if not denylist(line)] - filtered_lines = [line for line in article.lines - if not denylist(line)] - self.assertEqual(filtered_lines, filtered_head_lines + [b''] + body.lines) - - def test_capabilities(self): - # The server under test implements NNTP version 2 and has a - # couple of well-known capabilities. Just sanity check that we - # got them. - def _check_caps(caps): - caps_list = caps['LIST'] - self.assertIsInstance(caps_list, (list, tuple)) - self.assertIn('OVERVIEW.FMT', caps_list) - self.assertGreaterEqual(self.server.nntp_version, 2) - _check_caps(self.server.getcapabilities()) - # This re-emits the command - resp, caps = self.server.capabilities() - _check_caps(caps) - - def test_zlogin(self): - # This test must be the penultimate because further commands will be - # refused. - baduser = "notarealuser" - badpw = "notarealpassword" - # Check that bogus credentials cause failure - self.assertRaises(nntplib.NNTPError, self.server.login, - user=baduser, password=badpw, usenetrc=False) - # FIXME: We should check that correct credentials succeed, but that - # would require valid details for some server somewhere to be in the - # test suite, I think. Gmane is anonymous, at least as used for the - # other tests. - - def test_zzquit(self): - # This test must be called last, hence the name - cls = type(self) - try: - self.server.quit() - finally: - cls.server = None - - @classmethod - def wrap_methods(cls): - # Wrap all methods in a transient_internet() exception catcher - # XXX put a generic version in test.support? - def wrap_meth(meth): - @functools.wraps(meth) - def wrapped(self): - with socket_helper.transient_internet(self.NNTP_HOST): - meth(self) - return wrapped - for name in dir(cls): - if not name.startswith('test_'): - continue - meth = getattr(cls, name) - if not callable(meth): - continue - # Need to use a closure so that meth remains bound to its current - # value - setattr(cls, name, wrap_meth(meth)) - - def test_timeout(self): - with self.assertRaises(ValueError): - self.NNTP_CLASS(self.NNTP_HOST, timeout=0, usenetrc=False) - - def test_with_statement(self): - def is_connected(): - if not hasattr(server, 'file'): - return False - try: - server.help() - except (OSError, EOFError): - return False - return True - - kwargs = dict( - timeout=support.INTERNET_TIMEOUT, - usenetrc=False - ) - if self.ssl_context is not None: - kwargs["ssl_context"] = self.ssl_context - - try: - server = self.NNTP_CLASS(self.NNTP_HOST, **kwargs) - with server: - self.assertTrue(is_connected()) - self.assertTrue(server.help()) - self.assertFalse(is_connected()) - - server = self.NNTP_CLASS(self.NNTP_HOST, **kwargs) - with server: - server.quit() - self.assertFalse(is_connected()) - except SSLError as ssl_err: - # matches "[SSL: DH_KEY_TOO_SMALL] dh key too small" - if re.search(r'(?i)KEY.TOO.SMALL', ssl_err.reason): - raise unittest.SkipTest(f"Got {ssl_err} connecting " - f"to {self.NNTP_HOST!r}") - raise - - -NetworkedNNTPTestsMixin.wrap_methods() - - -EOF_ERRORS = (EOFError,) -if ssl is not None: - EOF_ERRORS += (ssl.SSLEOFError,) - - -class NetworkedNNTPTests(NetworkedNNTPTestsMixin, unittest.TestCase): - # This server supports STARTTLS (gmane doesn't) - NNTP_HOST = 'news.trigofacile.com' - GROUP_NAME = 'fr.comp.lang.python' - GROUP_PAT = 'fr.comp.lang.*' - DESC = 'Python' - - NNTP_CLASS = NNTP - - @classmethod - def setUpClass(cls): - support.requires("network") - kwargs = dict( - timeout=support.INTERNET_TIMEOUT, - usenetrc=False - ) - if cls.ssl_context is not None: - kwargs["ssl_context"] = cls.ssl_context - with socket_helper.transient_internet(cls.NNTP_HOST): - try: - cls.server = cls.NNTP_CLASS(cls.NNTP_HOST, **kwargs) - except SSLError as ssl_err: - # matches "[SSL: DH_KEY_TOO_SMALL] dh key too small" - if re.search(r'(?i)KEY.TOO.SMALL', ssl_err.reason): - raise unittest.SkipTest(f"{cls} got {ssl_err} connecting " - f"to {cls.NNTP_HOST!r}") - print(cls.NNTP_HOST) - raise - except EOF_ERRORS: - raise unittest.SkipTest(f"{cls} got EOF error on connecting " - f"to {cls.NNTP_HOST!r}") - - @classmethod - def tearDownClass(cls): - if cls.server is not None: - cls.server.quit() - -@unittest.skipUnless(ssl, 'requires SSL support') -class NetworkedNNTP_SSLTests(NetworkedNNTPTests): - - # Technical limits for this public NNTP server (see http://www.aioe.org): - # "Only two concurrent connections per IP address are allowed and - # 400 connections per day are accepted from each IP address." - - NNTP_HOST = 'nntp.aioe.org' - # bpo-42794: aioe.test is one of the official groups on this server - # used for testing: https://news.aioe.org/manual/aioe-hierarchy/ - GROUP_NAME = 'aioe.test' - GROUP_PAT = 'aioe.*' - DESC = 'test' - - NNTP_CLASS = getattr(nntplib, 'NNTP_SSL', None) - - # Disabled as it produces too much data - test_list = None - - # Disabled as the connection will already be encrypted. - test_starttls = None - - if ssl is not None: - ssl_context = ssl._create_unverified_context() - ssl_context.set_ciphers("DEFAULT") - ssl_context.maximum_version = ssl.TLSVersion.TLSv1_2 - -# -# Non-networked tests using a local server (or something mocking it). -# - -class _NNTPServerIO(io.RawIOBase): - """A raw IO object allowing NNTP commands to be received and processed - by a handler. The handler can push responses which can then be read - from the IO object.""" - - def __init__(self, handler): - io.RawIOBase.__init__(self) - # The channel from the client - self.c2s = io.BytesIO() - # The channel to the client - self.s2c = io.BytesIO() - self.handler = handler - self.handler.start(self.c2s.readline, self.push_data) - - def readable(self): - return True - - def writable(self): - return True - - def push_data(self, data): - """Push (buffer) some data to send to the client.""" - pos = self.s2c.tell() - self.s2c.seek(0, 2) - self.s2c.write(data) - self.s2c.seek(pos) - - def write(self, b): - """The client sends us some data""" - pos = self.c2s.tell() - self.c2s.write(b) - self.c2s.seek(pos) - self.handler.process_pending() - return len(b) - - def readinto(self, buf): - """The client wants to read a response""" - self.handler.process_pending() - b = self.s2c.read(len(buf)) - n = len(b) - buf[:n] = b - return n - - -def make_mock_file(handler): - sio = _NNTPServerIO(handler) - # Using BufferedRWPair instead of BufferedRandom ensures the file - # isn't seekable. - file = io.BufferedRWPair(sio, sio) - return (sio, file) - - -class NNTPServer(nntplib.NNTP): - - def __init__(self, f, host, readermode=None): - self.file = f - self.host = host - self._base_init(readermode) - - def _close(self): - self.file.close() - del self.file - - -class MockedNNTPTestsMixin: - # Override in derived classes - handler_class = None - - def setUp(self): - super().setUp() - self.make_server() - - def tearDown(self): - super().tearDown() - del self.server - - def make_server(self, *args, **kwargs): - self.handler = self.handler_class() - self.sio, file = make_mock_file(self.handler) - self.server = NNTPServer(file, 'test.server', *args, **kwargs) - return self.server - - -class MockedNNTPWithReaderModeMixin(MockedNNTPTestsMixin): - def setUp(self): - super().setUp() - self.make_server(readermode=True) - - -class NNTPv1Handler: - """A handler for RFC 977""" - - welcome = "200 NNTP mock server" - - def start(self, readline, push_data): - self.in_body = False - self.allow_posting = True - self._readline = readline - self._push_data = push_data - self._logged_in = False - self._user_sent = False - # Our welcome - self.handle_welcome() - - def _decode(self, data): - return str(data, "utf-8", "surrogateescape") - - def process_pending(self): - if self.in_body: - while True: - line = self._readline() - if not line: - return - self.body.append(line) - if line == b".\r\n": - break - try: - meth, tokens = self.body_callback - meth(*tokens, body=self.body) - finally: - self.body_callback = None - self.body = None - self.in_body = False - while True: - line = self._decode(self._readline()) - if not line: - return - if not line.endswith("\r\n"): - raise ValueError("line doesn't end with \\r\\n: {!r}".format(line)) - line = line[:-2] - cmd, *tokens = line.split() - #meth = getattr(self.handler, "handle_" + cmd.upper(), None) - meth = getattr(self, "handle_" + cmd.upper(), None) - if meth is None: - self.handle_unknown() - else: - try: - meth(*tokens) - except Exception as e: - raise ValueError("command failed: {!r}".format(line)) from e - else: - if self.in_body: - self.body_callback = meth, tokens - self.body = [] - - def expect_body(self): - """Flag that the client is expected to post a request body""" - self.in_body = True - - def push_data(self, data): - """Push some binary data""" - self._push_data(data) - - def push_lit(self, lit): - """Push a string literal""" - lit = textwrap.dedent(lit) - lit = "\r\n".join(lit.splitlines()) + "\r\n" - lit = lit.encode('utf-8') - self.push_data(lit) - - def handle_unknown(self): - self.push_lit("500 What?") - - def handle_welcome(self): - self.push_lit(self.welcome) - - def handle_QUIT(self): - self.push_lit("205 Bye!") - - def handle_DATE(self): - self.push_lit("111 20100914001155") - - def handle_GROUP(self, group): - if group == "fr.comp.lang.python": - self.push_lit("211 486 761 1265 fr.comp.lang.python") - else: - self.push_lit("411 No such group {}".format(group)) - - def handle_HELP(self): - self.push_lit("""\ - 100 Legal commands - authinfo user Name|pass Password|generic <prog> <args> - date - help - Report problems to <root@example.org> - .""") - - def handle_STAT(self, message_spec=None): - if message_spec is None: - self.push_lit("412 No newsgroup selected") - elif message_spec == "3000234": - self.push_lit("223 3000234 <45223423@example.com>") - elif message_spec == "<45223423@example.com>": - self.push_lit("223 0 <45223423@example.com>") - else: - self.push_lit("430 No Such Article Found") - - def handle_NEXT(self): - self.push_lit("223 3000237 <668929@example.org> retrieved") - - def handle_LAST(self): - self.push_lit("223 3000234 <45223423@example.com> retrieved") - - def handle_LIST(self, action=None, param=None): - if action is None: - self.push_lit("""\ - 215 Newsgroups in form "group high low flags". - comp.lang.python 0000052340 0000002828 y - comp.lang.python.announce 0000001153 0000000993 m - free.it.comp.lang.python 0000000002 0000000002 y - fr.comp.lang.python 0000001254 0000000760 y - free.it.comp.lang.python.learner 0000000000 0000000001 y - tw.bbs.comp.lang.python 0000000304 0000000304 y - .""") - elif action == "ACTIVE": - if param == "*distutils*": - self.push_lit("""\ - 215 Newsgroups in form "group high low flags" - gmane.comp.python.distutils.devel 0000014104 0000000001 m - gmane.comp.python.distutils.cvs 0000000000 0000000001 m - .""") - else: - self.push_lit("""\ - 215 Newsgroups in form "group high low flags" - .""") - elif action == "OVERVIEW.FMT": - self.push_lit("""\ - 215 Order of fields in overview database. - Subject: - From: - Date: - Message-ID: - References: - Bytes: - Lines: - Xref:full - .""") - elif action == "NEWSGROUPS": - assert param is not None - if param == "comp.lang.python": - self.push_lit("""\ - 215 Descriptions in form "group description". - comp.lang.python\tThe Python computer language. - .""") - elif param == "comp.lang.python*": - self.push_lit("""\ - 215 Descriptions in form "group description". - comp.lang.python.announce\tAnnouncements about the Python language. (Moderated) - comp.lang.python\tThe Python computer language. - .""") - else: - self.push_lit("""\ - 215 Descriptions in form "group description". - .""") - else: - self.push_lit('501 Unknown LIST keyword') - - def handle_NEWNEWS(self, group, date_str, time_str): - # We hard code different return messages depending on passed - # argument and date syntax. - if (group == "comp.lang.python" and date_str == "20100913" - and time_str == "082004"): - # Date was passed in RFC 3977 format (NNTP "v2") - self.push_lit("""\ - 230 list of newsarticles (NNTP v2) created after Mon Sep 13 08:20:04 2010 follows - <a4929a40-6328-491a-aaaf-cb79ed7309a2@q2g2000vbk.googlegroups.com> - <f30c0419-f549-4218-848f-d7d0131da931@y3g2000vbm.googlegroups.com> - .""") - elif (group == "comp.lang.python" and date_str == "100913" - and time_str == "082004"): - # Date was passed in RFC 977 format (NNTP "v1") - self.push_lit("""\ - 230 list of newsarticles (NNTP v1) created after Mon Sep 13 08:20:04 2010 follows - <a4929a40-6328-491a-aaaf-cb79ed7309a2@q2g2000vbk.googlegroups.com> - <f30c0419-f549-4218-848f-d7d0131da931@y3g2000vbm.googlegroups.com> - .""") - elif (group == 'comp.lang.python' and - date_str in ('20100101', '100101') and - time_str == '090000'): - self.push_lit('too long line' * 3000 + - '\n.') - else: - self.push_lit("""\ - 230 An empty list of newsarticles follows - .""") - # (Note for experiments: many servers disable NEWNEWS. - # As of this writing, sicinfo3.epfl.ch doesn't.) - - def handle_XOVER(self, message_spec): - if message_spec == "57-59": - self.push_lit( - "224 Overview information for 57-58 follows\n" - "57\tRe: ANN: New Plone book with strong Python (and Zope) themes throughout" - "\tDoug Hellmann <doug.hellmann-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>" - "\tSat, 19 Jun 2010 18:04:08 -0400" - "\t<4FD05F05-F98B-44DC-8111-C6009C925F0C@gmail.com>" - "\t<hvalf7$ort$1@dough.gmane.org>\t7103\t16" - "\tXref: news.gmane.io gmane.comp.python.authors:57" - "\n" - "58\tLooking for a few good bloggers" - "\tDoug Hellmann <doug.hellmann-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>" - "\tThu, 22 Jul 2010 09:14:14 -0400" - "\t<A29863FA-F388-40C3-AA25-0FD06B09B5BF@gmail.com>" - "\t\t6683\t16" - "\t" - "\n" - # A UTF-8 overview line from fr.comp.lang.python - "59\tRe: Message d'erreur incompréhensible (par moi)" - "\tEric Brunel <eric.brunel@pragmadev.nospam.com>" - "\tWed, 15 Sep 2010 18:09:15 +0200" - "\t<eric.brunel-2B8B56.18091515092010@news.wanadoo.fr>" - "\t<4c90ec87$0$32425$ba4acef3@reader.news.orange.fr>\t1641\t27" - "\tXref: saria.nerim.net fr.comp.lang.python:1265" - "\n" - ".\n") - else: - self.push_lit("""\ - 224 No articles - .""") - - def handle_POST(self, *, body=None): - if body is None: - if self.allow_posting: - self.push_lit("340 Input article; end with <CR-LF>.<CR-LF>") - self.expect_body() - else: - self.push_lit("440 Posting not permitted") - else: - assert self.allow_posting - self.push_lit("240 Article received OK") - self.posted_body = body - - def handle_IHAVE(self, message_id, *, body=None): - if body is None: - if (self.allow_posting and - message_id == "<i.am.an.article.you.will.want@example.com>"): - self.push_lit("335 Send it; end with <CR-LF>.<CR-LF>") - self.expect_body() - else: - self.push_lit("435 Article not wanted") - else: - assert self.allow_posting - self.push_lit("235 Article transferred OK") - self.posted_body = body - - sample_head = """\ - From: "Demo User" <nobody@example.net> - Subject: I am just a test article - Content-Type: text/plain; charset=UTF-8; format=flowed - Message-ID: <i.am.an.article.you.will.want@example.com>""" - - sample_body = """\ - This is just a test article. - ..Here is a dot-starting line. - - -- Signed by Andr\xe9.""" - - sample_article = sample_head + "\n\n" + sample_body - - def handle_ARTICLE(self, message_spec=None): - if message_spec is None: - self.push_lit("220 3000237 <45223423@example.com>") - elif message_spec == "<45223423@example.com>": - self.push_lit("220 0 <45223423@example.com>") - elif message_spec == "3000234": - self.push_lit("220 3000234 <45223423@example.com>") - else: - self.push_lit("430 No Such Article Found") - return - self.push_lit(self.sample_article) - self.push_lit(".") - - def handle_HEAD(self, message_spec=None): - if message_spec is None: - self.push_lit("221 3000237 <45223423@example.com>") - elif message_spec == "<45223423@example.com>": - self.push_lit("221 0 <45223423@example.com>") - elif message_spec == "3000234": - self.push_lit("221 3000234 <45223423@example.com>") - else: - self.push_lit("430 No Such Article Found") - return - self.push_lit(self.sample_head) - self.push_lit(".") - - def handle_BODY(self, message_spec=None): - if message_spec is None: - self.push_lit("222 3000237 <45223423@example.com>") - elif message_spec == "<45223423@example.com>": - self.push_lit("222 0 <45223423@example.com>") - elif message_spec == "3000234": - self.push_lit("222 3000234 <45223423@example.com>") - else: - self.push_lit("430 No Such Article Found") - return - self.push_lit(self.sample_body) - self.push_lit(".") - - def handle_AUTHINFO(self, cred_type, data): - if self._logged_in: - self.push_lit('502 Already Logged In') - elif cred_type == 'user': - if self._user_sent: - self.push_lit('482 User Credential Already Sent') - else: - self.push_lit('381 Password Required') - self._user_sent = True - elif cred_type == 'pass': - self.push_lit('281 Login Successful') - self._logged_in = True - else: - raise Exception('Unknown cred type {}'.format(cred_type)) - - -class NNTPv2Handler(NNTPv1Handler): - """A handler for RFC 3977 (NNTP "v2")""" - - def handle_CAPABILITIES(self): - fmt = """\ - 101 Capability list: - VERSION 2 3 - IMPLEMENTATION INN 2.5.1{} - HDR - LIST ACTIVE ACTIVE.TIMES DISTRIB.PATS HEADERS NEWSGROUPS OVERVIEW.FMT - OVER - POST - READER - .""" - - if not self._logged_in: - self.push_lit(fmt.format('\n AUTHINFO USER')) - else: - self.push_lit(fmt.format('')) - - def handle_MODE(self, _): - raise Exception('MODE READER sent despite READER has been advertised') - - def handle_OVER(self, message_spec=None): - return self.handle_XOVER(message_spec) - - -class CapsAfterLoginNNTPv2Handler(NNTPv2Handler): - """A handler that allows CAPABILITIES only after login""" - - def handle_CAPABILITIES(self): - if not self._logged_in: - self.push_lit('480 You must log in.') - else: - super().handle_CAPABILITIES() - - -class ModeSwitchingNNTPv2Handler(NNTPv2Handler): - """A server that starts in transit mode""" - - def __init__(self): - self._switched = False - - def handle_CAPABILITIES(self): - fmt = """\ - 101 Capability list: - VERSION 2 3 - IMPLEMENTATION INN 2.5.1 - HDR - LIST ACTIVE ACTIVE.TIMES DISTRIB.PATS HEADERS NEWSGROUPS OVERVIEW.FMT - OVER - POST - {}READER - .""" - if self._switched: - self.push_lit(fmt.format('')) - else: - self.push_lit(fmt.format('MODE-')) - - def handle_MODE(self, what): - assert not self._switched and what == 'reader' - self._switched = True - self.push_lit('200 Posting allowed') - - -class NNTPv1v2TestsMixin: - - def setUp(self): - super().setUp() - - def test_welcome(self): - self.assertEqual(self.server.welcome, self.handler.welcome) - - def test_authinfo(self): - if self.nntp_version == 2: - self.assertIn('AUTHINFO', self.server._caps) - self.server.login('testuser', 'testpw') - # if AUTHINFO is gone from _caps we also know that getcapabilities() - # has been called after login as it should - self.assertNotIn('AUTHINFO', self.server._caps) - - def test_date(self): - resp, date = self.server.date() - self.assertEqual(resp, "111 20100914001155") - self.assertEqual(date, datetime.datetime(2010, 9, 14, 0, 11, 55)) - - def test_quit(self): - self.assertFalse(self.sio.closed) - resp = self.server.quit() - self.assertEqual(resp, "205 Bye!") - self.assertTrue(self.sio.closed) - - def test_help(self): - resp, help = self.server.help() - self.assertEqual(resp, "100 Legal commands") - self.assertEqual(help, [ - ' authinfo user Name|pass Password|generic <prog> <args>', - ' date', - ' help', - 'Report problems to <root@example.org>', - ]) - - def test_list(self): - resp, groups = self.server.list() - self.assertEqual(len(groups), 6) - g = groups[1] - self.assertEqual(g, - GroupInfo("comp.lang.python.announce", "0000001153", - "0000000993", "m")) - resp, groups = self.server.list("*distutils*") - self.assertEqual(len(groups), 2) - g = groups[0] - self.assertEqual(g, - GroupInfo("gmane.comp.python.distutils.devel", "0000014104", - "0000000001", "m")) - - def test_stat(self): - resp, art_num, message_id = self.server.stat(3000234) - self.assertEqual(resp, "223 3000234 <45223423@example.com>") - self.assertEqual(art_num, 3000234) - self.assertEqual(message_id, "<45223423@example.com>") - resp, art_num, message_id = self.server.stat("<45223423@example.com>") - self.assertEqual(resp, "223 0 <45223423@example.com>") - self.assertEqual(art_num, 0) - self.assertEqual(message_id, "<45223423@example.com>") - with self.assertRaises(nntplib.NNTPTemporaryError) as cm: - self.server.stat("<non.existent.id>") - self.assertEqual(cm.exception.response, "430 No Such Article Found") - with self.assertRaises(nntplib.NNTPTemporaryError) as cm: - self.server.stat() - self.assertEqual(cm.exception.response, "412 No newsgroup selected") - - def test_next(self): - resp, art_num, message_id = self.server.next() - self.assertEqual(resp, "223 3000237 <668929@example.org> retrieved") - self.assertEqual(art_num, 3000237) - self.assertEqual(message_id, "<668929@example.org>") - - def test_last(self): - resp, art_num, message_id = self.server.last() - self.assertEqual(resp, "223 3000234 <45223423@example.com> retrieved") - self.assertEqual(art_num, 3000234) - self.assertEqual(message_id, "<45223423@example.com>") - - def test_description(self): - desc = self.server.description("comp.lang.python") - self.assertEqual(desc, "The Python computer language.") - desc = self.server.description("comp.lang.pythonx") - self.assertEqual(desc, "") - - def test_descriptions(self): - resp, groups = self.server.descriptions("comp.lang.python") - self.assertEqual(resp, '215 Descriptions in form "group description".') - self.assertEqual(groups, { - "comp.lang.python": "The Python computer language.", - }) - resp, groups = self.server.descriptions("comp.lang.python*") - self.assertEqual(groups, { - "comp.lang.python": "The Python computer language.", - "comp.lang.python.announce": "Announcements about the Python language. (Moderated)", - }) - resp, groups = self.server.descriptions("comp.lang.pythonx") - self.assertEqual(groups, {}) - - def test_group(self): - resp, count, first, last, group = self.server.group("fr.comp.lang.python") - self.assertTrue(resp.startswith("211 "), resp) - self.assertEqual(first, 761) - self.assertEqual(last, 1265) - self.assertEqual(count, 486) - self.assertEqual(group, "fr.comp.lang.python") - with self.assertRaises(nntplib.NNTPTemporaryError) as cm: - self.server.group("comp.lang.python.devel") - exc = cm.exception - self.assertTrue(exc.response.startswith("411 No such group"), - exc.response) - - def test_newnews(self): - # NEWNEWS comp.lang.python [20]100913 082004 - dt = datetime.datetime(2010, 9, 13, 8, 20, 4) - resp, ids = self.server.newnews("comp.lang.python", dt) - expected = ( - "230 list of newsarticles (NNTP v{0}) " - "created after Mon Sep 13 08:20:04 2010 follows" - ).format(self.nntp_version) - self.assertEqual(resp, expected) - self.assertEqual(ids, [ - "<a4929a40-6328-491a-aaaf-cb79ed7309a2@q2g2000vbk.googlegroups.com>", - "<f30c0419-f549-4218-848f-d7d0131da931@y3g2000vbm.googlegroups.com>", - ]) - # NEWNEWS fr.comp.lang.python [20]100913 082004 - dt = datetime.datetime(2010, 9, 13, 8, 20, 4) - resp, ids = self.server.newnews("fr.comp.lang.python", dt) - self.assertEqual(resp, "230 An empty list of newsarticles follows") - self.assertEqual(ids, []) - - def _check_article_body(self, lines): - self.assertEqual(len(lines), 4) - self.assertEqual(lines[-1].decode('utf-8'), "-- Signed by André.") - self.assertEqual(lines[-2], b"") - self.assertEqual(lines[-3], b".Here is a dot-starting line.") - self.assertEqual(lines[-4], b"This is just a test article.") - - def _check_article_head(self, lines): - self.assertEqual(len(lines), 4) - self.assertEqual(lines[0], b'From: "Demo User" <nobody@example.net>') - self.assertEqual(lines[3], b"Message-ID: <i.am.an.article.you.will.want@example.com>") - - def _check_article_data(self, lines): - self.assertEqual(len(lines), 9) - self._check_article_head(lines[:4]) - self._check_article_body(lines[-4:]) - self.assertEqual(lines[4], b"") - - def test_article(self): - # ARTICLE - resp, info = self.server.article() - self.assertEqual(resp, "220 3000237 <45223423@example.com>") - art_num, message_id, lines = info - self.assertEqual(art_num, 3000237) - self.assertEqual(message_id, "<45223423@example.com>") - self._check_article_data(lines) - # ARTICLE num - resp, info = self.server.article(3000234) - self.assertEqual(resp, "220 3000234 <45223423@example.com>") - art_num, message_id, lines = info - self.assertEqual(art_num, 3000234) - self.assertEqual(message_id, "<45223423@example.com>") - self._check_article_data(lines) - # ARTICLE id - resp, info = self.server.article("<45223423@example.com>") - self.assertEqual(resp, "220 0 <45223423@example.com>") - art_num, message_id, lines = info - self.assertEqual(art_num, 0) - self.assertEqual(message_id, "<45223423@example.com>") - self._check_article_data(lines) - # Non-existent id - with self.assertRaises(nntplib.NNTPTemporaryError) as cm: - self.server.article("<non-existent@example.com>") - self.assertEqual(cm.exception.response, "430 No Such Article Found") - - def test_article_file(self): - # With a "file" argument - f = io.BytesIO() - resp, info = self.server.article(file=f) - self.assertEqual(resp, "220 3000237 <45223423@example.com>") - art_num, message_id, lines = info - self.assertEqual(art_num, 3000237) - self.assertEqual(message_id, "<45223423@example.com>") - self.assertEqual(lines, []) - data = f.getvalue() - self.assertTrue(data.startswith( - b'From: "Demo User" <nobody@example.net>\r\n' - b'Subject: I am just a test article\r\n' - ), ascii(data)) - self.assertTrue(data.endswith( - b'This is just a test article.\r\n' - b'.Here is a dot-starting line.\r\n' - b'\r\n' - b'-- Signed by Andr\xc3\xa9.\r\n' - ), ascii(data)) - - def test_head(self): - # HEAD - resp, info = self.server.head() - self.assertEqual(resp, "221 3000237 <45223423@example.com>") - art_num, message_id, lines = info - self.assertEqual(art_num, 3000237) - self.assertEqual(message_id, "<45223423@example.com>") - self._check_article_head(lines) - # HEAD num - resp, info = self.server.head(3000234) - self.assertEqual(resp, "221 3000234 <45223423@example.com>") - art_num, message_id, lines = info - self.assertEqual(art_num, 3000234) - self.assertEqual(message_id, "<45223423@example.com>") - self._check_article_head(lines) - # HEAD id - resp, info = self.server.head("<45223423@example.com>") - self.assertEqual(resp, "221 0 <45223423@example.com>") - art_num, message_id, lines = info - self.assertEqual(art_num, 0) - self.assertEqual(message_id, "<45223423@example.com>") - self._check_article_head(lines) - # Non-existent id - with self.assertRaises(nntplib.NNTPTemporaryError) as cm: - self.server.head("<non-existent@example.com>") - self.assertEqual(cm.exception.response, "430 No Such Article Found") - - def test_head_file(self): - f = io.BytesIO() - resp, info = self.server.head(file=f) - self.assertEqual(resp, "221 3000237 <45223423@example.com>") - art_num, message_id, lines = info - self.assertEqual(art_num, 3000237) - self.assertEqual(message_id, "<45223423@example.com>") - self.assertEqual(lines, []) - data = f.getvalue() - self.assertTrue(data.startswith( - b'From: "Demo User" <nobody@example.net>\r\n' - b'Subject: I am just a test article\r\n' - ), ascii(data)) - self.assertFalse(data.endswith( - b'This is just a test article.\r\n' - b'.Here is a dot-starting line.\r\n' - b'\r\n' - b'-- Signed by Andr\xc3\xa9.\r\n' - ), ascii(data)) - - def test_body(self): - # BODY - resp, info = self.server.body() - self.assertEqual(resp, "222 3000237 <45223423@example.com>") - art_num, message_id, lines = info - self.assertEqual(art_num, 3000237) - self.assertEqual(message_id, "<45223423@example.com>") - self._check_article_body(lines) - # BODY num - resp, info = self.server.body(3000234) - self.assertEqual(resp, "222 3000234 <45223423@example.com>") - art_num, message_id, lines = info - self.assertEqual(art_num, 3000234) - self.assertEqual(message_id, "<45223423@example.com>") - self._check_article_body(lines) - # BODY id - resp, info = self.server.body("<45223423@example.com>") - self.assertEqual(resp, "222 0 <45223423@example.com>") - art_num, message_id, lines = info - self.assertEqual(art_num, 0) - self.assertEqual(message_id, "<45223423@example.com>") - self._check_article_body(lines) - # Non-existent id - with self.assertRaises(nntplib.NNTPTemporaryError) as cm: - self.server.body("<non-existent@example.com>") - self.assertEqual(cm.exception.response, "430 No Such Article Found") - - def test_body_file(self): - f = io.BytesIO() - resp, info = self.server.body(file=f) - self.assertEqual(resp, "222 3000237 <45223423@example.com>") - art_num, message_id, lines = info - self.assertEqual(art_num, 3000237) - self.assertEqual(message_id, "<45223423@example.com>") - self.assertEqual(lines, []) - data = f.getvalue() - self.assertFalse(data.startswith( - b'From: "Demo User" <nobody@example.net>\r\n' - b'Subject: I am just a test article\r\n' - ), ascii(data)) - self.assertTrue(data.endswith( - b'This is just a test article.\r\n' - b'.Here is a dot-starting line.\r\n' - b'\r\n' - b'-- Signed by Andr\xc3\xa9.\r\n' - ), ascii(data)) - - def check_over_xover_resp(self, resp, overviews): - self.assertTrue(resp.startswith("224 "), resp) - self.assertEqual(len(overviews), 3) - art_num, over = overviews[0] - self.assertEqual(art_num, 57) - self.assertEqual(over, { - "from": "Doug Hellmann <doug.hellmann-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>", - "subject": "Re: ANN: New Plone book with strong Python (and Zope) themes throughout", - "date": "Sat, 19 Jun 2010 18:04:08 -0400", - "message-id": "<4FD05F05-F98B-44DC-8111-C6009C925F0C@gmail.com>", - "references": "<hvalf7$ort$1@dough.gmane.org>", - ":bytes": "7103", - ":lines": "16", - "xref": "news.gmane.io gmane.comp.python.authors:57" - }) - art_num, over = overviews[1] - self.assertEqual(over["xref"], None) - art_num, over = overviews[2] - self.assertEqual(over["subject"], - "Re: Message d'erreur incompréhensible (par moi)") - - def test_xover(self): - resp, overviews = self.server.xover(57, 59) - self.check_over_xover_resp(resp, overviews) - - def test_over(self): - # In NNTP "v1", this will fallback on XOVER - resp, overviews = self.server.over((57, 59)) - self.check_over_xover_resp(resp, overviews) - - sample_post = ( - b'From: "Demo User" <nobody@example.net>\r\n' - b'Subject: I am just a test article\r\n' - b'Content-Type: text/plain; charset=UTF-8; format=flowed\r\n' - b'Message-ID: <i.am.an.article.you.will.want@example.com>\r\n' - b'\r\n' - b'This is just a test article.\r\n' - b'.Here is a dot-starting line.\r\n' - b'\r\n' - b'-- Signed by Andr\xc3\xa9.\r\n' - ) - - def _check_posted_body(self): - # Check the raw body as received by the server - lines = self.handler.posted_body - # One additional line for the "." terminator - self.assertEqual(len(lines), 10) - self.assertEqual(lines[-1], b'.\r\n') - self.assertEqual(lines[-2], b'-- Signed by Andr\xc3\xa9.\r\n') - self.assertEqual(lines[-3], b'\r\n') - self.assertEqual(lines[-4], b'..Here is a dot-starting line.\r\n') - self.assertEqual(lines[0], b'From: "Demo User" <nobody@example.net>\r\n') - - def _check_post_ihave_sub(self, func, *args, file_factory): - # First the prepared post with CRLF endings - post = self.sample_post - func_args = args + (file_factory(post),) - self.handler.posted_body = None - resp = func(*func_args) - self._check_posted_body() - # Then the same post with "normal" line endings - they should be - # converted by NNTP.post and NNTP.ihave. - post = self.sample_post.replace(b"\r\n", b"\n") - func_args = args + (file_factory(post),) - self.handler.posted_body = None - resp = func(*func_args) - self._check_posted_body() - return resp - - def check_post_ihave(self, func, success_resp, *args): - # With a bytes object - resp = self._check_post_ihave_sub(func, *args, file_factory=bytes) - self.assertEqual(resp, success_resp) - # With a bytearray object - resp = self._check_post_ihave_sub(func, *args, file_factory=bytearray) - self.assertEqual(resp, success_resp) - # With a file object - resp = self._check_post_ihave_sub(func, *args, file_factory=io.BytesIO) - self.assertEqual(resp, success_resp) - # With an iterable of terminated lines - def iterlines(b): - return iter(b.splitlines(keepends=True)) - resp = self._check_post_ihave_sub(func, *args, file_factory=iterlines) - self.assertEqual(resp, success_resp) - # With an iterable of non-terminated lines - def iterlines(b): - return iter(b.splitlines(keepends=False)) - resp = self._check_post_ihave_sub(func, *args, file_factory=iterlines) - self.assertEqual(resp, success_resp) - - def test_post(self): - self.check_post_ihave(self.server.post, "240 Article received OK") - self.handler.allow_posting = False - with self.assertRaises(nntplib.NNTPTemporaryError) as cm: - self.server.post(self.sample_post) - self.assertEqual(cm.exception.response, - "440 Posting not permitted") - - def test_ihave(self): - self.check_post_ihave(self.server.ihave, "235 Article transferred OK", - "<i.am.an.article.you.will.want@example.com>") - with self.assertRaises(nntplib.NNTPTemporaryError) as cm: - self.server.ihave("<another.message.id>", self.sample_post) - self.assertEqual(cm.exception.response, - "435 Article not wanted") - - def test_too_long_lines(self): - dt = datetime.datetime(2010, 1, 1, 9, 0, 0) - self.assertRaises(nntplib.NNTPDataError, - self.server.newnews, "comp.lang.python", dt) - - -class NNTPv1Tests(NNTPv1v2TestsMixin, MockedNNTPTestsMixin, unittest.TestCase): - """Tests an NNTP v1 server (no capabilities).""" - - nntp_version = 1 - handler_class = NNTPv1Handler - - def test_caps(self): - caps = self.server.getcapabilities() - self.assertEqual(caps, {}) - self.assertEqual(self.server.nntp_version, 1) - self.assertEqual(self.server.nntp_implementation, None) - - -class NNTPv2Tests(NNTPv1v2TestsMixin, MockedNNTPTestsMixin, unittest.TestCase): - """Tests an NNTP v2 server (with capabilities).""" - - nntp_version = 2 - handler_class = NNTPv2Handler - - def test_caps(self): - caps = self.server.getcapabilities() - self.assertEqual(caps, { - 'VERSION': ['2', '3'], - 'IMPLEMENTATION': ['INN', '2.5.1'], - 'AUTHINFO': ['USER'], - 'HDR': [], - 'LIST': ['ACTIVE', 'ACTIVE.TIMES', 'DISTRIB.PATS', - 'HEADERS', 'NEWSGROUPS', 'OVERVIEW.FMT'], - 'OVER': [], - 'POST': [], - 'READER': [], - }) - self.assertEqual(self.server.nntp_version, 3) - self.assertEqual(self.server.nntp_implementation, 'INN 2.5.1') - - -class CapsAfterLoginNNTPv2Tests(MockedNNTPTestsMixin, unittest.TestCase): - """Tests a probably NNTP v2 server with capabilities only after login.""" - - nntp_version = 2 - handler_class = CapsAfterLoginNNTPv2Handler - - def test_caps_only_after_login(self): - self.assertEqual(self.server._caps, {}) - self.server.login('testuser', 'testpw') - self.assertIn('VERSION', self.server._caps) - - -class SendReaderNNTPv2Tests(MockedNNTPWithReaderModeMixin, - unittest.TestCase): - """Same tests as for v2 but we tell NTTP to send MODE READER to a server - that isn't in READER mode by default.""" - - nntp_version = 2 - handler_class = ModeSwitchingNNTPv2Handler - - def test_we_are_in_reader_mode_after_connect(self): - self.assertIn('READER', self.server._caps) - - -class MiscTests(unittest.TestCase): - - def test_decode_header(self): - def gives(a, b): - self.assertEqual(nntplib.decode_header(a), b) - gives("" , "") - gives("a plain header", "a plain header") - gives(" with extra spaces ", " with extra spaces ") - gives("=?ISO-8859-15?Q?D=E9buter_en_Python?=", "Débuter en Python") - gives("=?utf-8?q?Re=3A_=5Bsqlite=5D_probl=C3=A8me_avec_ORDER_BY_sur_des_cha?=" - " =?utf-8?q?=C3=AEnes_de_caract=C3=A8res_accentu=C3=A9es?=", - "Re: [sqlite] problème avec ORDER BY sur des chaînes de caractères accentuées") - gives("Re: =?UTF-8?B?cHJvYmzDqG1lIGRlIG1hdHJpY2U=?=", - "Re: problème de matrice") - # A natively utf-8 header (found in the real world!) - gives("Re: Message d'erreur incompréhensible (par moi)", - "Re: Message d'erreur incompréhensible (par moi)") - - def test_parse_overview_fmt(self): - # The minimal (default) response - lines = ["Subject:", "From:", "Date:", "Message-ID:", - "References:", ":bytes", ":lines"] - self.assertEqual(nntplib._parse_overview_fmt(lines), - ["subject", "from", "date", "message-id", "references", - ":bytes", ":lines"]) - # The minimal response using alternative names - lines = ["Subject:", "From:", "Date:", "Message-ID:", - "References:", "Bytes:", "Lines:"] - self.assertEqual(nntplib._parse_overview_fmt(lines), - ["subject", "from", "date", "message-id", "references", - ":bytes", ":lines"]) - # Variations in casing - lines = ["subject:", "FROM:", "DaTe:", "message-ID:", - "References:", "BYTES:", "Lines:"] - self.assertEqual(nntplib._parse_overview_fmt(lines), - ["subject", "from", "date", "message-id", "references", - ":bytes", ":lines"]) - # First example from RFC 3977 - lines = ["Subject:", "From:", "Date:", "Message-ID:", - "References:", ":bytes", ":lines", "Xref:full", - "Distribution:full"] - self.assertEqual(nntplib._parse_overview_fmt(lines), - ["subject", "from", "date", "message-id", "references", - ":bytes", ":lines", "xref", "distribution"]) - # Second example from RFC 3977 - lines = ["Subject:", "From:", "Date:", "Message-ID:", - "References:", "Bytes:", "Lines:", "Xref:FULL", - "Distribution:FULL"] - self.assertEqual(nntplib._parse_overview_fmt(lines), - ["subject", "from", "date", "message-id", "references", - ":bytes", ":lines", "xref", "distribution"]) - # A classic response from INN - lines = ["Subject:", "From:", "Date:", "Message-ID:", - "References:", "Bytes:", "Lines:", "Xref:full"] - self.assertEqual(nntplib._parse_overview_fmt(lines), - ["subject", "from", "date", "message-id", "references", - ":bytes", ":lines", "xref"]) - - def test_parse_overview(self): - fmt = nntplib._DEFAULT_OVERVIEW_FMT + ["xref"] - # First example from RFC 3977 - lines = [ - '3000234\tI am just a test article\t"Demo User" ' - '<nobody@example.com>\t6 Oct 1998 04:38:40 -0500\t' - '<45223423@example.com>\t<45454@example.net>\t1234\t' - '17\tXref: news.example.com misc.test:3000363', - ] - overview = nntplib._parse_overview(lines, fmt) - (art_num, fields), = overview - self.assertEqual(art_num, 3000234) - self.assertEqual(fields, { - 'subject': 'I am just a test article', - 'from': '"Demo User" <nobody@example.com>', - 'date': '6 Oct 1998 04:38:40 -0500', - 'message-id': '<45223423@example.com>', - 'references': '<45454@example.net>', - ':bytes': '1234', - ':lines': '17', - 'xref': 'news.example.com misc.test:3000363', - }) - # Second example; here the "Xref" field is totally absent (including - # the header name) and comes out as None - lines = [ - '3000234\tI am just a test article\t"Demo User" ' - '<nobody@example.com>\t6 Oct 1998 04:38:40 -0500\t' - '<45223423@example.com>\t<45454@example.net>\t1234\t' - '17\t\t', - ] - overview = nntplib._parse_overview(lines, fmt) - (art_num, fields), = overview - self.assertEqual(fields['xref'], None) - # Third example; the "Xref" is an empty string, while "references" - # is a single space. - lines = [ - '3000234\tI am just a test article\t"Demo User" ' - '<nobody@example.com>\t6 Oct 1998 04:38:40 -0500\t' - '<45223423@example.com>\t \t1234\t' - '17\tXref: \t', - ] - overview = nntplib._parse_overview(lines, fmt) - (art_num, fields), = overview - self.assertEqual(fields['references'], ' ') - self.assertEqual(fields['xref'], '') - - def test_parse_datetime(self): - def gives(a, b, *c): - self.assertEqual(nntplib._parse_datetime(a, b), - datetime.datetime(*c)) - # Output of DATE command - gives("19990623135624", None, 1999, 6, 23, 13, 56, 24) - # Variations - gives("19990623", "135624", 1999, 6, 23, 13, 56, 24) - gives("990623", "135624", 1999, 6, 23, 13, 56, 24) - gives("090623", "135624", 2009, 6, 23, 13, 56, 24) - - def test_unparse_datetime(self): - # Test non-legacy mode - # 1) with a datetime - def gives(y, M, d, h, m, s, date_str, time_str): - dt = datetime.datetime(y, M, d, h, m, s) - self.assertEqual(nntplib._unparse_datetime(dt), - (date_str, time_str)) - self.assertEqual(nntplib._unparse_datetime(dt, False), - (date_str, time_str)) - gives(1999, 6, 23, 13, 56, 24, "19990623", "135624") - gives(2000, 6, 23, 13, 56, 24, "20000623", "135624") - gives(2010, 6, 5, 1, 2, 3, "20100605", "010203") - # 2) with a date - def gives(y, M, d, date_str, time_str): - dt = datetime.date(y, M, d) - self.assertEqual(nntplib._unparse_datetime(dt), - (date_str, time_str)) - self.assertEqual(nntplib._unparse_datetime(dt, False), - (date_str, time_str)) - gives(1999, 6, 23, "19990623", "000000") - gives(2000, 6, 23, "20000623", "000000") - gives(2010, 6, 5, "20100605", "000000") - - def test_unparse_datetime_legacy(self): - # Test legacy mode (RFC 977) - # 1) with a datetime - def gives(y, M, d, h, m, s, date_str, time_str): - dt = datetime.datetime(y, M, d, h, m, s) - self.assertEqual(nntplib._unparse_datetime(dt, True), - (date_str, time_str)) - gives(1999, 6, 23, 13, 56, 24, "990623", "135624") - gives(2000, 6, 23, 13, 56, 24, "000623", "135624") - gives(2010, 6, 5, 1, 2, 3, "100605", "010203") - # 2) with a date - def gives(y, M, d, date_str, time_str): - dt = datetime.date(y, M, d) - self.assertEqual(nntplib._unparse_datetime(dt, True), - (date_str, time_str)) - gives(1999, 6, 23, "990623", "000000") - gives(2000, 6, 23, "000623", "000000") - gives(2010, 6, 5, "100605", "000000") - - @unittest.skipUnless(ssl, 'requires SSL support') - def test_ssl_support(self): - self.assertTrue(hasattr(nntplib, 'NNTP_SSL')) - - -class PublicAPITests(unittest.TestCase): - """Ensures that the correct values are exposed in the public API.""" - - def test_module_all_attribute(self): - self.assertTrue(hasattr(nntplib, '__all__')) - target_api = ['NNTP', 'NNTPError', 'NNTPReplyError', - 'NNTPTemporaryError', 'NNTPPermanentError', - 'NNTPProtocolError', 'NNTPDataError', 'decode_header'] - if ssl is not None: - target_api.append('NNTP_SSL') - self.assertEqual(set(nntplib.__all__), set(target_api)) - -class MockSocketTests(unittest.TestCase): - """Tests involving a mock socket object - - Used where the _NNTPServerIO file object is not enough.""" - - nntp_class = nntplib.NNTP - - def check_constructor_error_conditions( - self, handler_class, - expected_error_type, expected_error_msg, - login=None, password=None): - - class mock_socket_module: - def create_connection(address, timeout): - return MockSocket() - - class MockSocket: - def close(self): - nonlocal socket_closed - socket_closed = True - - def makefile(socket, mode): - handler = handler_class() - _, file = make_mock_file(handler) - files.append(file) - return file - - socket_closed = False - files = [] - with patch('nntplib.socket', mock_socket_module), \ - self.assertRaisesRegex(expected_error_type, expected_error_msg): - self.nntp_class('dummy', user=login, password=password) - self.assertTrue(socket_closed) - for f in files: - self.assertTrue(f.closed) - - def test_bad_welcome(self): - #Test a bad welcome message - class Handler(NNTPv1Handler): - welcome = 'Bad Welcome' - self.check_constructor_error_conditions( - Handler, nntplib.NNTPProtocolError, Handler.welcome) - - def test_service_temporarily_unavailable(self): - #Test service temporarily unavailable - class Handler(NNTPv1Handler): - welcome = '400 Service temporarily unavailable' - self.check_constructor_error_conditions( - Handler, nntplib.NNTPTemporaryError, Handler.welcome) - - def test_service_permanently_unavailable(self): - #Test service permanently unavailable - class Handler(NNTPv1Handler): - welcome = '502 Service permanently unavailable' - self.check_constructor_error_conditions( - Handler, nntplib.NNTPPermanentError, Handler.welcome) - - def test_bad_capabilities(self): - #Test a bad capabilities response - class Handler(NNTPv1Handler): - def handle_CAPABILITIES(self): - self.push_lit(capabilities_response) - capabilities_response = '201 bad capability' - self.check_constructor_error_conditions( - Handler, nntplib.NNTPReplyError, capabilities_response) - - def test_login_aborted(self): - #Test a bad authinfo response - login = 't@e.com' - password = 'python' - class Handler(NNTPv1Handler): - def handle_AUTHINFO(self, *args): - self.push_lit(authinfo_response) - authinfo_response = '503 Mechanism not recognized' - self.check_constructor_error_conditions( - Handler, nntplib.NNTPPermanentError, authinfo_response, - login, password) - -class bypass_context: - """Bypass encryption and actual SSL module""" - def wrap_socket(sock, **args): - return sock - -@unittest.skipUnless(ssl, 'requires SSL support') -class MockSslTests(MockSocketTests): - @staticmethod - def nntp_class(*pos, **kw): - return nntplib.NNTP_SSL(*pos, ssl_context=bypass_context, **kw) - - -class LocalServerTests(unittest.TestCase): - def setUp(self): - sock = socket.socket() - port = socket_helper.bind_port(sock) - sock.listen() - self.background = threading.Thread( - target=self.run_server, args=(sock,)) - self.background.start() - self.addCleanup(self.background.join) - - self.nntp = self.enterContext(NNTP(socket_helper.HOST, port, usenetrc=False)) - - def run_server(self, sock): - # Could be generalized to handle more commands in separate methods - with sock: - [client, _] = sock.accept() - with contextlib.ExitStack() as cleanup: - cleanup.enter_context(client) - reader = cleanup.enter_context(client.makefile('rb')) - client.sendall(b'200 Server ready\r\n') - while True: - cmd = reader.readline() - if cmd == b'CAPABILITIES\r\n': - client.sendall( - b'101 Capability list:\r\n' - b'VERSION 2\r\n' - b'STARTTLS\r\n' - b'.\r\n' - ) - elif cmd == b'STARTTLS\r\n': - reader.close() - client.sendall(b'382 Begin TLS negotiation now\r\n') - context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) - context.load_cert_chain(certfile) - client = context.wrap_socket( - client, server_side=True) - cleanup.enter_context(client) - reader = cleanup.enter_context(client.makefile('rb')) - elif cmd == b'QUIT\r\n': - client.sendall(b'205 Bye!\r\n') - break - else: - raise ValueError('Unexpected command {!r}'.format(cmd)) - - @unittest.skipUnless(ssl, 'requires SSL support') - def test_starttls(self): - file = self.nntp.file - sock = self.nntp.sock - self.nntp.starttls() - # Check that the socket and internal pseudo-file really were - # changed. - self.assertNotEqual(file, self.nntp.file) - self.assertNotEqual(sock, self.nntp.sock) - # Check that the new socket really is an SSL one - self.assertIsInstance(self.nntp.sock, ssl.SSLSocket) - # Check that trying starttls when it's already active fails. - self.assertRaises(ValueError, self.nntp.starttls) - - -if __name__ == "__main__": - unittest.main() diff --git a/Makefile.pre.in b/Makefile.pre.in index c24e8aa..b277092 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -1873,7 +1873,7 @@ QUICKTESTOPTS= $(TESTOPTS) -x test_subprocess test_io \ test_multibytecodec test_urllib2_localnet test_itertools \ test_multiprocessing_fork test_multiprocessing_spawn \ test_multiprocessing_forkserver \ - test_mailbox test_nntplib test_socket test_poll \ + test_mailbox test_socket test_poll \ test_select test_zipfile test_concurrent_futures .PHONY: quicktest diff --git a/Misc/NEWS.d/3.9.0a1.rst b/Misc/NEWS.d/3.9.0a1.rst index 94c3a37..f75d422 100644 --- a/Misc/NEWS.d/3.9.0a1.rst +++ b/Misc/NEWS.d/3.9.0a1.rst @@ -98,7 +98,7 @@ after whitespace, e.g. '127.0.0.1 whatever'. .. section: Security Adds audit events for :mod:`ensurepip`, :mod:`ftplib`, :mod:`glob`, -:mod:`imaplib`, :mod:`nntplib`, :mod:`pdb`, :mod:`poplib`, :mod:`shutil`, +:mod:`imaplib`, :mod:`!nntplib`, :mod:`pdb`, :mod:`poplib`, :mod:`shutil`, :mod:`smtplib`, :mod:`sqlite3`, :mod:`subprocess`, :mod:`!telnetlib`, :mod:`tempfile` and :mod:`webbrowser`, as well as :func:`os.listdir`, :func:`os.scandir` and :func:`breakpoint`. diff --git a/Misc/NEWS.d/3.9.0a3.rst b/Misc/NEWS.d/3.9.0a3.rst index b05b4c3..8b7ff49 100644 --- a/Misc/NEWS.d/3.9.0a3.rst +++ b/Misc/NEWS.d/3.9.0a3.rst @@ -337,7 +337,7 @@ than always signaling maximum compression. .. section: Library The previously deprecated ``xpath()`` and ``xgtitle()`` methods of -:class:`nntplib.NNTP` have been removed. +:class:`!nntplib.NNTP` have been removed. .. @@ -454,7 +454,7 @@ resilients to inaccessible sys.path entries (importlib_metadata v1.4.0). .. nonce: _S5VjC .. section: Library -:class:`~nntplib.NNTP` and :class:`~nntplib.NNTP_SSL` now raise a +:class:`~!nntplib.NNTP` and :class:`~!nntplib.NNTP_SSL` now raise a :class:`ValueError` if the given timeout for their constructor is zero to prevent the creation of a non-blocking socket. Patch by Dong-hee Na. @@ -498,7 +498,7 @@ prevent the creation of a non-blocking socket. Patch by Dong-hee Na. .. section: Library Updated the Gmane domain from news.gmane.org to news.gmane.io which is used -for examples of :class:`~nntplib.NNTP` news reader server and nntplib tests. +for examples of :class:`~!nntplib.NNTP` news reader server and nntplib tests. .. diff --git a/Misc/NEWS.d/3.9.0b1.rst b/Misc/NEWS.d/3.9.0b1.rst index a7f52f8..15790bc 100644 --- a/Misc/NEWS.d/3.9.0b1.rst +++ b/Misc/NEWS.d/3.9.0b1.rst @@ -489,8 +489,8 @@ The first argument of :func:`pickle.loads` is now positional-only. .. nonce: Cuwu_H .. section: Library -Update :mod:`nntplib` to merge :class:`nntplib.NNTP` and -:class:`nntplib._NNTPBase`. Patch by Dong-hee Na. +Update :mod:`!nntplib` to merge :class:`!nntplib.NNTP` and +:class:`!nntplib._NNTPBase`. Patch by Dong-hee Na. .. diff --git a/Misc/NEWS.d/next/Library/2023-05-24-22-22-03.gh-issue-104773.NwpjhZ.rst b/Misc/NEWS.d/next/Library/2023-05-24-22-22-03.gh-issue-104773.NwpjhZ.rst new file mode 100644 index 0000000..f995375 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-05-24-22-22-03.gh-issue-104773.NwpjhZ.rst @@ -0,0 +1,2 @@ +:pep:`594`: Remove the :mod:`!nntplib` module, deprecated in Python 3.11. +Patch by Victor Stinner. diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h index d91fea3..f27a801 100644 --- a/Python/stdlib_module_names.h +++ b/Python/stdlib_module_names.h @@ -185,7 +185,6 @@ static const char* _Py_stdlib_module_names[] = { "multiprocessing", "netrc", "nis", -"nntplib", "nt", "ntpath", "nturl2path", diff --git a/Tools/wasm/wasm_assets.py b/Tools/wasm/wasm_assets.py index 34340ad..4da30f2 100755 --- a/Tools/wasm/wasm_assets.py +++ b/Tools/wasm/wasm_assets.py @@ -69,7 +69,6 @@ OMIT_NETWORKING_FILES = ( "http/", "imaplib.py", "mailbox.py", - "nntplib.py", "poplib.py", "smtplib.py", "socketserver.py", |