diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index ea7f7f95..1d820a35 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + python-version: [3.7, 3.8, 3.9, 3.11, 3.12] steps: - name: Checkout code diff --git a/docs/source/powerline_autodoc.py b/docs/source/powerline_autodoc.py index eba42edb..83996aea 100644 --- a/docs/source/powerline_autodoc.py +++ b/docs/source/powerline_autodoc.py @@ -3,11 +3,9 @@ from __future__ import (unicode_literals, division, absolute_import, print_funct import os -from inspect import formatargspec - from sphinx.ext import autodoc -from powerline.lint.inspect import getconfigargspec +from powerline.lint.inspect import formatconfigargspec, getconfigargspec from powerline.segments import Segment from powerline.lib.unicode import unicode @@ -28,7 +26,7 @@ class ThreadedDocumenter(autodoc.FunctionDocumenter): def format_args(self): argspec = getconfigargspec(self.object) - return formatargspec(*argspec, formatvalue=formatvalue).replace('\\', '\\\\') + return formatconfigargspec(*argspec, formatvalue=formatvalue).replace('\\', '\\\\') class Repr(object): diff --git a/powerline/bindings/config.py b/powerline/bindings/config.py index 31006330..eb6693b9 100644 --- a/powerline/bindings/config.py +++ b/powerline/bindings/config.py @@ -176,7 +176,7 @@ def init_tmux_environment(pl, args, set_tmux_environment=set_tmux_environment): ' ' * powerline.renderer.strwidth(left_dividers['hard']))) -TMUX_VAR_RE = re.compile('\$(_POWERLINE_\w+)') +TMUX_VAR_RE = re.compile(r'\$(_POWERLINE_\w+)') def tmux_setup(pl, args): diff --git a/powerline/lib/encoding.py b/powerline/lib/encoding.py index 76a51d81..d5f42605 100644 --- a/powerline/lib/encoding.py +++ b/powerline/lib/encoding.py @@ -46,12 +46,12 @@ def get_preferred_output_encoding(): if hasattr(locale, 'LC_MESSAGES'): return ( locale.getlocale(locale.LC_MESSAGES)[1] - or locale.getdefaultlocale()[1] + or locale.getlocale()[1] or 'ascii' ) return ( - locale.getdefaultlocale()[1] + locale.getlocale()[1] or 'ascii' ) @@ -66,12 +66,12 @@ def get_preferred_input_encoding(): if hasattr(locale, 'LC_MESSAGES'): return ( locale.getlocale(locale.LC_MESSAGES)[1] - or locale.getdefaultlocale()[1] + or locale.getlocale()[1] or 'latin1' ) return ( - locale.getdefaultlocale()[1] + locale.getlocale()[1] or 'latin1' ) @@ -86,7 +86,7 @@ def get_preferred_arguments_encoding(): a problem. ''' return ( - locale.getdefaultlocale()[1] + locale.getlocale()[1] or 'latin1' ) diff --git a/powerline/lint/__init__.py b/powerline/lint/__init__.py index 8c682718..76ba0fd5 100644 --- a/powerline/lint/__init__.py +++ b/powerline/lint/__init__.py @@ -41,7 +41,7 @@ def generate_json_config_loader(lhadproblem): return load_json_config -function_name_re = '^(\w+\.)*[a-zA-Z_]\w*$' +function_name_re = r'^(\w+\.)*[a-zA-Z_]\w*$' divider_spec = Spec().printable().len( @@ -205,7 +205,7 @@ vim_colorscheme_spec = (Spec( mode_translations_value_spec(), ).optional().context_message('Error while loading mode translations (key {key})'), ).context_message('Error while loading vim colorscheme')) -shell_mode_spec = Spec().re('^(?:[\w\-]+|\.safe)$').copy +shell_mode_spec = Spec().re(r'^(?:[\w\-]+|\.safe)$').copy shell_colorscheme_spec = (Spec( name=name_spec(), groups=groups_spec(), @@ -223,7 +223,7 @@ args_spec = Spec( segment_module_spec = Spec().type(unicode).func(check_segment_module).optional().copy exinclude_spec = Spec().re(function_name_re).func(check_exinclude_function).copy segment_spec_base = Spec( - name=Spec().re('^[a-zA-Z_]\w*$').optional(), + name=Spec().re(r'^[a-zA-Z_]\w*$').optional(), function=Spec().re(function_name_re).func(check_segment_function).optional(), exclude_modes=Spec().list(vim_mode_spec()).optional(), include_modes=Spec().list(vim_mode_spec()).optional(), diff --git a/powerline/lint/inspect.py b/powerline/lint/inspect.py index 15bb6103..7ab720f8 100644 --- a/powerline/lint/inspect.py +++ b/powerline/lint/inspect.py @@ -1,7 +1,8 @@ # vim:fileencoding=utf-8:noet from __future__ import (unicode_literals, division, absolute_import, print_function) -from inspect import ArgSpec, getargspec +from inspect import FullArgSpec, getfullargspec +from itertools import zip_longest from powerline.segments import Segment @@ -33,7 +34,7 @@ def getconfigargspec(obj): requires_filesystem_watcher = hasattr(obj, 'powerline_requires_filesystem_watcher') for name, method in argspecobjs: - argspec = getargspec(method) + argspec = getfullargspec(method) omitted_args = get_omitted_args(name, method) largs = len(argspec.args) for i, arg in enumerate(reversed(argspec.args)): @@ -60,4 +61,31 @@ def getconfigargspec(obj): if arg not in args: args.insert(0, arg) - return ArgSpec(args=args, varargs=None, keywords=None, defaults=tuple(defaults)) + return FullArgSpec(args=args, varargs=None, varkw=None, defaults=tuple(defaults), kwonlyargs=(), kwonlydefaults={}, annotations={}) + + +def formatconfigargspec(args, varargs=None, varkw=None, defaults=None, + kwonlyargs=(), kwonlydefaults={}, annotations={}, + formatvalue=lambda value: '=' + repr(value)): + '''Format an argument spec from the values returned by getconfigargspec. + + This is a specialized replacement for inspect.formatargspec, which has been + deprecated since Python 3.5 and was removed in Python 3.11. It supports + valid values for args, defaults and formatvalue; all other parameters are + expected to be either empty or None. + ''' + assert varargs is None + assert varkw is None + assert not kwonlyargs + assert not kwonlydefaults + assert not annotations + + specs = [] + if defaults: + firstdefault = len(args) - len(defaults) + for i, arg in enumerate(args): + spec = arg + if defaults and i >= firstdefault: + spec += formatvalue(defaults[i - firstdefault]) + specs.append(spec) + return '(' + ', '.join(specs) + ')' diff --git a/powerline/lint/spec.py b/powerline/lint/spec.py index 6a54441c..e39bfde0 100644 --- a/powerline/lint/spec.py +++ b/powerline/lint/spec.py @@ -34,7 +34,7 @@ class Spec(object): .. note:: Methods that create the specifications return ``self``, so calls to them - may be chained: ``Spec().type(unicode).re('^\w+$')``. This does not + may be chained: ``Spec().type(unicode).re('^\\w+$')``. This does not apply to functions that *apply* specification like :py:meth`Spec.match`. .. note:: @@ -585,7 +585,7 @@ class Spec(object): msg_func or (lambda value: 'String "{0}" is not an alphanumeric/underscore colon-separated identifier'.format(value)) ) - return self.re('^\w+(?::\w+)?$', msg_func) + return self.re(r'^\w+(?::\w+)?$', msg_func) def oneof(self, collection, msg_func=None): '''Describe value that is equal to one of the value in the collection diff --git a/powerline/segments/common/mail.py b/powerline/segments/common/mail.py index 82024921..07577a8d 100644 --- a/powerline/segments/common/mail.py +++ b/powerline/segments/common/mail.py @@ -1,6 +1,8 @@ # vim:fileencoding=utf-8:noet from __future__ import (unicode_literals, division, absolute_import, print_function) +import os + import re from imaplib import IMAP4_SSL_PORT, IMAP4_SSL, IMAP4 @@ -17,9 +19,19 @@ class EmailIMAPSegment(KwThreadedSegment): interval = 60 @staticmethod - def key(username, password, server='imap.gmail.com', port=IMAP4_SSL_PORT, folder='INBOX', use_ssl=None, **kwargs): + def key(username='', password='', server='imap.gmail.com', port=IMAP4_SSL_PORT, username_variable='', password_variable='', server_variable='', port_variable='', folder='INBOX', use_ssl=None, **kwargs): if use_ssl is None: use_ssl = (port == IMAP4_SSL_PORT) + # catch if user set custom mail credential env variables + if username_variable: + username = os.environ[username_variable] + if password_variable: + password = os.environ[password_variable] + if server_variable: + server = os.environ[server_variable] + if port_variable: + port = os.environ[port_variable] + return _IMAPKey(username, password, server, port, folder, use_ssl) def compute_state(self, key): @@ -33,7 +45,7 @@ class EmailIMAPSegment(KwThreadedSegment): mail.login(key.username, key.password) rc, message = mail.status(key.folder, '(UNSEEN)') unread_str = message[0].decode('utf-8') - unread_count = int(re.search('UNSEEN (\d+)', unread_str).group(1)) + unread_count = int(re.search(r'UNSEEN (\d+)', unread_str).group(1)) return unread_count @staticmethod @@ -64,6 +76,14 @@ email_imap_alert = with_docstring(EmailIMAPSegment(), e-mail server :param int port: e-mail server port +:param str username_variable: + name of environment variable to check for login username +:param str password_variable: + name of environment variable to check for login password +:param str server_variable: + name of environment variable to check for email server +:param str port_variable: + name of environment variable to check for email server port :param str folder: folder to check for e-mails :param int max_msgs: diff --git a/powerline/segments/common/time.py b/powerline/segments/common/time.py index be727c9e..b7aaa0c9 100644 --- a/powerline/segments/common/time.py +++ b/powerline/segments/common/time.py @@ -1,7 +1,5 @@ # vim:fileencoding=utf-8:noet -from __future__ import (unicode_literals, division, absolute_import, print_function) - -from datetime import datetime +from datetime import datetime, timezone def date(pl, format='%Y-%m-%d', istime=False, timezone=None): @@ -45,11 +43,11 @@ UNICODE_TEXT_TRANSLATION = { } -def fuzzy_time(pl, format='{minute_str} {hour_str}', unicode_text=False, timezone=None, hour_str=['twelve', 'one', 'two', 'three', 'four', +def fuzzy_time(pl, format=None, unicode_text=False, timezone=None, hour_str=['twelve', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven'], minute_str = { - '0': 'o\'clock', '5': 'five past', '10': 'ten past','15': 'quarter past', - '20': 'twenty past', '25': 'twenty-five past', '30': 'half past', '35': 'twenty-five to', - '40': 'twenty to', '45': 'quarter to', '50': 'ten to', '55': 'five to' + '0': '{hour_str} o\'clock', '5': 'five past {hour_str}', '10': 'ten past {hour_str}','15': 'quarter past {hour_str}', + '20': 'twenty past {hour_str}', '25': 'twenty-five past {hour_str}', '30': 'half past {hour_str}', '35': 'twenty-five to {hour_str}', + '40': 'twenty to {hour_str}', '45': 'quarter to {hour_str}', '50': 'ten to {hour_str}', '55': 'five to {hour_str}' }, special_case_str = { '(23, 58)': 'round about midnight', '(23, 59)': 'round about midnight', @@ -62,8 +60,7 @@ def fuzzy_time(pl, format='{minute_str} {hour_str}', unicode_text=False, timezon '''Display the current time as fuzzy time, e.g. "quarter past six". :param string format: - Format used to display the fuzzy time. (Ignored when a special time - is displayed.) + (unused) :param bool unicode_text: If true then hyphenminuses (regular ASCII ``-``) and single quotes are replaced with unicode dashes and apostrophes. @@ -74,7 +71,9 @@ def fuzzy_time(pl, format='{minute_str} {hour_str}', unicode_text=False, timezon Strings to be used to display the hour, starting with midnight. (This list may contain 12 or 24 entries.) :param dict minute_str: - Dictionary mapping minutes to strings to be used to display them. + Dictionary mapping minutes to strings to be used to display them. Each entry may + optionally come with a format field "{hour_str}" to indicate the position of the + string used for the current hour. :param dict special_case_str: Special strings for special times. @@ -100,7 +99,7 @@ def fuzzy_time(pl, format='{minute_str} {hour_str}', unicode_text=False, timezon pass hour = now.hour - if now.minute >= 30: + if now.minute >= 32: hour = hour + 1 hour = hour % len(hour_str) @@ -115,7 +114,7 @@ def fuzzy_time(pl, format='{minute_str} {hour_str}', unicode_text=False, timezon elif now.minute < mn and mn - now.minute < min_dis: min_dis = mn - now.minute min_pos = mn - result = format.format(minute_str=minute_str[str(min_pos)], hour_str=hour_str[hour]) + result = minute_str[str(min_pos)].format(hour_str=hour_str[hour]) if unicode_text: result = result.translate(UNICODE_TEXT_TRANSLATION) diff --git a/powerline/version.py b/powerline/version.py index f380c80b..421844ee 100644 --- a/powerline/version.py +++ b/powerline/version.py @@ -1,7 +1,7 @@ # vim:fileencoding=utf-8:noet from __future__ import (unicode_literals, division, absolute_import, print_function) -__version__ = "2.8.3" +__version__ = "2.8.4" def get_version(): return __version__ diff --git a/tests/modules/lib/__init__.py b/tests/modules/lib/__init__.py index f6df9204..89790584 100644 --- a/tests/modules/lib/__init__.py +++ b/tests/modules/lib/__init__.py @@ -1,7 +1,7 @@ # vim:fileencoding=utf-8:noet from __future__ import (unicode_literals, division, absolute_import, print_function) -import imp +import types import sys @@ -48,7 +48,7 @@ def urllib_read(query_url): elif query_url.startswith('https://freegeoip.app/json/'): return '{"ip":"82.145.55.16","country_code":"DE","country_name":"Germany","region_code":"NI","region_name":"Lower Saxony","city":"Meppen","zip_code":"49716","time_zone":"Europe/Berlin","latitude":52.6833,"longitude":7.3167,"metro_code":0}' elif query_url.startswith('http://geoip.nekudo.com/api/'): - return '{"city":"Meppen","country":{"name":"Germany", "code":"DE"},"location":{"accuracy_radius":100,"latitude":52.6833,"longitude":7.3167,"time_zone":"Europe\/Berlin"},"ip":"82.145.55.16"}' + return r'{"city":"Meppen","country":{"name":"Germany", "code":"DE"},"location":{"accuracy_radius":100,"latitude":52.6833,"longitude":7.3167,"time_zone":"Europe\/Berlin"},"ip":"82.145.55.16"}' elif query_url.startswith('http://query.yahooapis.com/v1/public/'): if 'Meppen' in query_url or '52.6833' in query_url: return r'{"query":{"count":1,"created":"2016-05-13T19:43:18Z","lang":"en-US","results":{"channel":{"units":{"distance":"mi","pressure":"in","speed":"mph","temperature":"C"},"title":"Yahoo! Weather - Meppen, NI, DE","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-674836/","description":"Yahoo! Weather for Meppen, NI, DE","language":"en-us","lastBuildDate":"Fri, 13 May 2016 09:43 PM CEST","ttl":"60","location":{"city":"Meppen","country":"Germany","region":" NI"},"wind":{"chill":"55","direction":"350","speed":"25"},"atmosphere":{"humidity":"57","pressure":"1004.0","rising":"0","visibility":"16.1"},"astronomy":{"sunrise":"5:35 am","sunset":"9:21 pm"},"image":{"title":"Yahoo! Weather","width":"142","height":"18","link":"http://weather.yahoo.com","url":"http://l.yimg.com/a/i/brand/purplelogo//uh/us/news-wea.gif"},"item":{"title":"Conditions for Meppen, NI, DE at 08:00 PM CEST","lat":"52.68993","long":"7.29115","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-674836/","pubDate":"Fri, 13 May 2016 08:00 PM CEST","condition":{"code":"23","date":"Fri, 13 May 2016 08:00 PM CEST","temp":"14","text":"Breezy"},"forecast":[{"code":"30","date":"13 May 2016","day":"Fri","high":"71","low":"48","text":"Partly Cloudy"},{"code":"28","date":"14 May 2016","day":"Sat","high":"54","low":"44","text":"Mostly Cloudy"},{"code":"11","date":"15 May 2016","day":"Sun","high":"55","low":"43","text":"Showers"},{"code":"28","date":"16 May 2016","day":"Mon","high":"54","low":"42","text":"Mostly Cloudy"},{"code":"28","date":"17 May 2016","day":"Tue","high":"57","low":"43","text":"Mostly Cloudy"},{"code":"12","date":"18 May 2016","day":"Wed","high":"62","low":"45","text":"Rain"},{"code":"28","date":"19 May 2016","day":"Thu","high":"63","low":"48","text":"Mostly Cloudy"},{"code":"28","date":"20 May 2016","day":"Fri","high":"67","low":"50","text":"Mostly Cloudy"},{"code":"30","date":"21 May 2016","day":"Sat","high":"71","low":"50","text":"Partly Cloudy"},{"code":"30","date":"22 May 2016","day":"Sun","high":"74","low":"54","text":"Partly Cloudy"}],"description":"\n
\nCurrent Conditions:\n
Breezy\n
\n
\nForecast:\n
Fri - Partly Cloudy. High: 71Low: 48\n
Sat - Mostly Cloudy. High: 54Low: 44\n
Sun - Showers. High: 55Low: 43\n
Mon - Mostly Cloudy. High: 54Low: 42\n
Tue - Mostly Cloudy. High: 57Low: 43\n
\n
\nFull Forecast at Yahoo! Weather\n
\n
\n(provided by The Weather Channel)\n
\n]]>","guid":{"isPermaLink":"false"}}}}}}' @@ -100,7 +100,7 @@ def replace_module(name, new=None, **kwargs): def new_module(name, **kwargs): - module = imp.new_module(name) + module = types.ModuleType(name) for k, v in kwargs.items(): setattr(module, k, v) return module diff --git a/tests/test_python/test_provided_config_files.py b/tests/test_python/test_provided_config_files.py index fd8b16e6..ca08a6d5 100644 --- a/tests/test_python/test_provided_config_files.py +++ b/tests/test_python/test_provided_config_files.py @@ -111,7 +111,7 @@ class TestVimConfig(TestCase): class TestConfig(TestCase): def test_tmux(self): from powerline.segments import common - from imp import reload + from importlib import reload reload(common) from powerline.shell import ShellPowerline with replace_attr(common, 'urllib_read', urllib_read): @@ -165,7 +165,7 @@ class TestConfig(TestCase): def test_wm(self): from powerline.segments import common - from imp import reload + from importlib import reload reload(common) from powerline import Powerline with replace_attr(wthr, 'urllib_read', urllib_read): diff --git a/tests/test_python/test_segments.py b/tests/test_python/test_segments.py index c7a9870a..cc39fbac 100644 --- a/tests/test_python/test_segments.py +++ b/tests/test_python/test_segments.py @@ -843,17 +843,16 @@ class TestTime(TestCommon): time = Args(hour=0, minute=45) pl = Pl() hour_str = ['12', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven'] - minute_str = {'0': 'o\'clock', '5': 'five past', '10': 'ten past','15': - 'quarter past','20': 'twenty past', '25': 'twenty-five past', - '30': 'half past', '35': 'twenty-five to','40': 'twenty to', '45': - 'quarter to', '50': 'ten to', '55': 'five to'} + minute_str = {'0': '{hour_str} o\'clock', '5': 'five past {hour_str}', '10': 'ten past {hour_str}', + '15': 'quarter past {hour_str}', '20': 'twenty past {hour_str}', '25': 'twenty-five past {hour_str}', + '30': 'half past {hour_str}', '35': 'twenty-five to {hour_str}', '40': 'twenty to {hour_str}', + '45': 'quarter to {hour_str}', '50': 'ten to {hour_str}', '55': 'five to {hour_str}'} special_case_str = { '(23, 58)': '~ midnight', '(23, 59)': '~ midnight', '(0, 0)': 'midnight', '(0, 1)': '~ midnight', - '(0, 2)': '~ midnight', - '(12, 0)': 'twelve o\'clock'} + '(0, 2)': '~ midnight'} with replace_attr(self.module, 'datetime', Args(strptime=lambda timezone, fmt: Args(tzinfo=timezone), now=lambda tz: time)): self.assertEqual(self.module.fuzzy_time(pl=pl, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), 'quarter to one') self.assertEqual(self.module.fuzzy_time(pl=pl), 'quarter to one') @@ -867,7 +866,7 @@ class TestTime(TestCommon): self.assertEqual(self.module.fuzzy_time(pl=pl), 'twenty-five to twelve') time.hour = 12 time.minute = 0 - self.assertEqual(self.module.fuzzy_time(pl=pl, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), 'twelve o\'clock') + self.assertEqual(self.module.fuzzy_time(pl=pl, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), '12 o\'clock') self.assertEqual(self.module.fuzzy_time(pl=pl), 'noon') time.hour = 11 time.minute = 33 @@ -875,7 +874,7 @@ class TestTime(TestCommon): self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=False), 'twenty-five to twelve') time.hour = 12 time.minute = 0 - self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=False, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), 'twelve o\'clock') + self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=False, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), '12 o\'clock') self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=False), 'noon') time.hour = 11 time.minute = 33 @@ -883,7 +882,7 @@ class TestTime(TestCommon): self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=True), 'twenty‐five to twelve') time.hour = 12 time.minute = 0 - self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=True, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), 'twelve o’clock') + self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=True, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), '12 o’clock') self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=True),'noon')