From 6c39a5cfa88c41f3ae0b404cf53e2fbf7072418f Mon Sep 17 00:00:00 2001 From: Riccardo M Date: Mon, 5 Sep 2016 00:16:00 +0200 Subject: [PATCH 1/5] Create qrspecial.py --- pyqrcode/qrspecial.py | 698 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 698 insertions(+) create mode 100644 pyqrcode/qrspecial.py diff --git a/pyqrcode/qrspecial.py b/pyqrcode/qrspecial.py new file mode 100644 index 0000000..f883e45 --- /dev/null +++ b/pyqrcode/qrspecial.py @@ -0,0 +1,698 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Generation of special-purpose text for Qr codes. +""" + +# Copyright (c) 2016, Riccardo Metere +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# ====================================================================== +# :: Future Imports +from __future__ import division +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + + +# ====================================================================== +# :: Python Standard Library Imports + + +# ====================================================================== +class QrSpecial(object): + """ + Special-purpose text for QR codes. + + Implements the special text generated by the ZXing project for QR codes. + Likely, these are correctly handled by software using the this library. + + Of note: + + - the `Event` special text is not supported here, but it can be handled by + using the `icalendar` package [https://pypi.python.org/pypi/icalendar]. + - the `vCard` contact format is not supported here (only MeCard), + but a number of packages for handling vCards are available in PyPI. + """ + _kv_sep = ':' + _sep = ';' + _token_fmt = '{{}}{kv_sep}{{}}'.format(kv_sep=_kv_sep) + _end_tag = ';;' + + _start_tag = '' + _fields = () + _field_tags = {} + _multi_fields = () + + _is_simple = True + + # ================================== + def __init__(self, **kws): + """ + Base class to encode/decode special-purpose text for QR codes. + + This class is not meant to be used directly, except for the 'parse' + method, which can be used as a constructor for the base classes. + + Args: + **kws (dict): Input info to generate the QrSpecial object. + """ + cls = type(self) + cls._is_simple = len(cls._field_tags) == 0 + # cls._fields = signature(cls).parameters.keys() # Py3-only + # first argument is always `self` + cls._fields = cls.__init__.__code__.co_varnames[ + 1:cls.__init__.__code__.co_argcount] + self.data = {} + for field in cls._fields: + if field in kws: + if kws[field] is not None: + self.data[field] = str(kws[field]) \ + if cls._is_simple or field not in cls._multi_fields \ + else cls._to_list(kws[field]) + else: + raise NameError('Invalid keyword argument ``'.format(field)) + + # ================================== + def __str__(self): + """ + Generate the human-friendly representation of the object. + + Returns: + text (str|unicode): The representation of the object. + """ + cls = type(self) + if cls._is_simple: + lines = [ + '{}: {}'.format(field, str(self.data[field])) + for field in cls._fields if field in self.data] + text = cls.__name__ + '\n' + '\n'.join(lines) + else: + lines = [] + for field in cls._fields: + if field in self.data: + if isinstance(self.data[field], list): + values = self.data[field] + for value in values: + lines.append('{}: {}'.format(field, str(value))) + else: + lines.append('{}: {}'.format( + field, str(self.data[field]))) + text = (cls.__name__ + '\n' + '\n'.join(lines)) + return text + + # ================================== + def __eq__( + self, + other): + """ + Generic implementation of equality. + + Args: + other: The object to compare to. + + Returns: + result (bool): True if contains the same data, False otherwise. + """ + if isinstance(other, type(self)): + return self.__dict__ == other.__dict__ + else: + return False + + # ================================== + def __ne__( + self, + other): + """ + Generic implementation of inequality. + + Args: + other: The object to compare to. + + Returns: + result (bool): False if contains the same data, True otherwise. + """ + return not self.__eq__(other) + + # ================================== + def to_str(self): + """ + Generate QR-ready text, i.e. the text to be used in QR codes. + + Returns: + text (str|unicode): The output text. + """ + cls = type(self) + if cls._is_simple: + values = [ + str(self.data[field]) + for field in cls._fields if field in self.data] + text = cls._start_tag + cls._sep.join(values) + else: + tokens = [] + for field in cls._fields: + if field in self.data: + if isinstance(self.data[field], list): + tokens.extend( + [cls._token_fmt.format( + cls._field_tags[field], str(value)) + for value in self.data[field]]) + else: + tokens.append(cls._token_fmt.format( + cls._field_tags[field], str(self.data[field]))) + + text = ( + cls._start_tag + cls._sep.join(tokens) + cls._end_tag) + return text + + # ================================== + def is_empty(self): + """ + Determines if the object contains data. + + Returns: + result (bool): True if the object contains data, False otherwise. + """ + return not len(self.data) > 0 + + # ================================== + @classmethod + def from_str(cls, text, strict=True, strip=True): + """ + Construct a QrSpecial object from its QR-ready text. + + This is conceptually the inverse operation of the 'to_str' method. + + Args: + text (str|unicode): The input text. + strict (bool): Raises an error if tags are missing. + strip (bool): Strip from whitespaces before parsing. + + Returns: + obj (QrSpecial): The QrSpecial object. + """ + if strip: + text = text.strip() + kws = {} + if text: + if text.startswith(cls._start_tag): + text = text[len(cls._start_tag):] + elif strict: + raise ValueError( + 'Starting tag `{}` not found'.format(cls._start_tag)) + if text.endswith(cls._end_tag): + text = text[:-len(cls._end_tag)] + elif strict and not cls._is_simple: + raise ValueError( + 'Ending tag `{}` not found'.format(cls._end_tag)) + tokens = text.split(cls._sep) + if cls._is_simple: + kws = {field: token + for field, token in zip(cls._fields, tokens)} + else: + fields = {v: k for k, v in cls._field_tags.items()} + for token in tokens: + tag, value = token.split(cls._kv_sep, 1) + if tag in fields: + if fields[tag] not in kws: + kws[fields[tag]] = value \ + if fields[tag] not in cls._multi_fields \ + else [value] + elif fields[tag] in cls._multi_fields: + kws[fields[tag]].append(value) + else: + raise KeyError( + 'Field `{}` already used'.format(fields[tag])) + else: + raise ValueError( + 'Unknown field identifier `{}`'.format(tag)) + return cls(**kws) + + # ================================== + @staticmethod + def _to_list(obj): + """ + Ensure that an object is a list. + + If the object is already a list, nothing is done. + Otherwise, if it is a tuple, it is converted to a list, else a list + cointaining the object (with only 1 element) is returned. + + Args: + obj: The input object. + + Returns: + result (list): a list-ified version of the object. + """ + if isinstance(obj, list): + return obj + elif isinstance(obj, tuple): + return list(obj) + else: + return [obj] + + # ================================== + @staticmethod + def parse(text): + """ + Construct a QrSpecial-derived object from a text. + + This can be useful for determining whether a given input is a valid + QrSpecial-derived object. + + Args: + text (str|unicode): The input text. + + Returns: + obj (QrSpecial): The QrSpecial-derived object. + """ + # Construct a QrSpecial + subclss = ( + QrPhone, QrEmail, QrMessage, QrGeolocation, QrUrl, + QrContact, QrWifi) + obj = QrSpecial() + for cls in subclss: + try: + obj = cls.from_str(text) + except ValueError: + pass + if obj is not None and not obj.is_empty(): + break + return obj + + +# ====================================================================== +class QrPhone(QrSpecial): + """ + QrSpecial-derived telephone number. + """ + _start_tag = 'tel:' + + # ================================== + def __init__( + self, + number=None): + """ + Generate the QrSpecial-derived telephone number. + + Args: + number (str|unicode|int): The telephone number. + + Examples: + >>> qrs = QrPhone('+39070653263') + >>> print(qrs) + QrPhone + number: +39070653263 + >>> print(qrs.to_str()) + tel:+39070653263 + >>> qrs == QrPhone.from_str(qrs.to_str()) + True + """ + # QrSpecial.__init__(**locals()) # Py3-only + kws = locals() + kws.pop('self') + QrSpecial.__init__(self, **kws) + + +# ====================================================================== +class QrEmail(QrSpecial): + """ + QrSpecial-derived e-mail address. + """ + _start_tag = 'mailto:' + + # ================================== + def __init__( + self, + email=None): + """ + Generate the QrSpecial-derived e-mail address. + + Args: + email (str|unicode): The e-mail address. + + Examples: + >>> qrs = QrEmail('spam@python.org') + >>> print(qrs) + QrEmail + email: spam@python.org + >>> print(qrs.to_str()) + mailto:spam@python.org + >>> qrs == QrEmail.from_str(qrs.to_str()) + True + """ + # QrSpecial.__init__(**locals()) # Py3-only + kws = locals() + kws.pop('self') + QrSpecial.__init__(self, **kws) + + +# ====================================================================== +class QrMessage(QrSpecial): + """ + QrSpecial-derived short message (SMS). + """ + _start_tag = 'smsto:' + _sep = ':' + + # ================================== + def __init__( + self, + number=None, + text=None): + """ + Generate the QrSpecial-derived short message (SMS). + + Args: + number (str|unicode|int): The number to send the text to. + text (str|unicode): The text to send. + + Examples: + >>> qrs = QrMessage('+39070653263', 'I like your code!') + >>> print(qrs) + QrMessage + number: +39070653263 + text: I like your code! + >>> print(qrs.to_str()) + smsto:+39070653263:I like your code! + >>> qrs == QrMessage.from_str(qrs.to_str()) + True + """ + # QrSpecial.__init__(**locals()) # Py3-only + kws = locals() + kws.pop('self') + QrSpecial.__init__(self, **kws) + + +# ====================================================================== +class QrGeolocation(QrSpecial): + """ + QrSpecial-derived geolocation. + """ + _start_tag = 'geo:' + _query_tag = '?q=' + _sep = ',' + + # ================================== + def __init__( + self, + lat=None, + lon=None, + query=None): + """ + Generate the QrSpecial-derived short message (SMS). + + Args: + lat (str|unicode|float): The latitude coordinate. + lon (str|unicode|float): The longitude coordinate. + query (str|unicode): The associated query. + It is present in ZXing QR implementation, but its usage is not + well documented. + Examples: + >>> qrs = QrGeolocation(42.989, -71.465, 'www.python.org') + >>> print(qrs) + QrGeolocation + lat: 42.989 + lon: -71.465 + query: www.python.org + >>> print(qrs.to_str()) + geo:42.989,-71.465?q=www.python.org + >>> qrs == QrGeolocation.from_str(qrs.to_str()) + True + >>> print(QrGeolocation(47.68, -122.121)) + QrGeolocation + lat: 47.68 + lon: -122.121 + """ + if lat is None: + lat = 0.0 + if lon is None: + lon = 0.0 + # QrSpecial.__init__(**locals()) # Py3-only + kws = locals() + kws.pop('self') + QrSpecial.__init__(self, **kws) + + # ================================== + def to_str(self): + cls = type(self) + text = QrSpecial.to_str(self) + first_sep_pos = text.find(cls._sep) + last_sep_pos = text.rfind(cls._sep) + if first_sep_pos != last_sep_pos: + text = ( + text[:last_sep_pos] + cls._query_tag + + text[last_sep_pos + len(cls._sep):]) + return text + + # ================================== + @classmethod + def from_str(cls, text, strict=True, strip=True): + # replace only first occurrence of the query tag + text = text.replace(cls._query_tag, cls._sep, 1) + return super(QrGeolocation, cls).from_str(text, strict, strip) + + +# ====================================================================== +class QrUrl(QrSpecial): + """ + QrSpecial-derived URL. + """ + _start_tag = 'http://' + + # ================================== + def __init__( + self, + url=None): + """ + Generate the QrSpecial-derived e-mail address. + + Args: + url (str|unicode): The e-mail address. + + Examples: + >>> qrs = QrUrl('http://www.python.org') + >>> print(qrs) + QrUrl + url: www.python.org + >>> print(qrs.to_str()) + http://www.python.org + >>> qrs == QrUrl.from_str(qrs.to_str()) + True + """ + cls = type(self) + if url is not None and url.startswith(cls._start_tag): + url = url[len(cls._start_tag):] + # QrSpecial.__init__(**locals()) # Py3-only + kws = locals() + kws.pop('self') + QrSpecial.__init__(self, **kws) + + +# ====================================================================== +class QrContact(QrSpecial): + """ + QrSpecial-derived contact information (MeCard). + """ + _start_tag = 'MECARD:' + _field_tags = dict(( + ('name', 'N'), + ('reading', 'SOUND'), + ('tel', 'TEL'), + ('telav', 'TEL-AV'), + ('email', 'EMAIL'), + ('memo', 'NOTE'), + ('birthday', 'BDAY'), + ('address', 'ADR'), + ('url', 'URL'), + ('nickname', 'NICKNAME'), + ('company', 'ORG'), + )) + + _multi_fields = { + 'tel', 'telav', 'email', 'address', 'url', 'nickname', 'company'} + + # ================================== + def __init__( + self, + name=None, + reading=None, + tel=None, + telav=None, + email=None, + memo=None, + birthday=None, + address=None, + url=None, + nickname=None, + company=None): + """ + Generate the QrSpecial-derived contact information (MeCard). + + This is implementation contains both elements of the NTT Docomo + specification (1) and the ZXing implementation (2). + In particular: + + - (1) allow for some fields to have multiple entries. + - (2) defines some additional fields, in analogy to vCard. + - (1) specify the i-mode compatible bar code recognition + function version; this is indicated for each field. + + Args: + name (str|unicode): The contact name. + When a field is divided by a comma (,), the first half is + treated as the last name and the second half is treated as + the first name. This is not enforced by this implementation. + NTT Docomo v.>=1. + reading (str|unicode): The name pronunciation. + When a field is divided by a comma (,), the first half is + treated as the last name and the second half is treated as + the first name. This is not enforced by this implementation. + NTT Docomo v.>=1. + tel (str|unicode|iterable): The contact phone number. + Multiple entries are allowed and should be inserted as a list. + Expects 1 to 24 digits, but this is not enforced. + NTT Docomo v.>=1. + telav (str|unicode|iterable): The contact video-phone number. + This is meant to specify a preferental number for video calls. + Multiple entries are allowed and should be inserted as a list. + Expects 1 to 24 digits, but this is not enforced. + NTT Docomo v.>=2. + email (str|unicode|iterable): The contact e-mail address. + Multiple entries are allowed and should be inserted as a list. + NTT Docomo v.>=1. + memo (str|unicode): A memo text. + NTT Docomo v.>=1. + birthday (str|unicode|int): The contact birthday. + The ISO 8601 basic format (date only) is expected: YYYYMMDD. + This comply with the NTT Docomo specifications. + NTT Docomo v.>=3. + address (str|unicode|iterable): The contact address. + The fields divided by commas (,) denote PO box, room number, + house number, city, prefecture, zip code and country, in order. + This is not enforced by this implementation. + NTT Docomo v.>=3. + url (str|unicode|iterable): The contact website. + NTT Docomo v.>=3. + nickname (str|unicode|iterable): The contact nickname. + NTT Docomo v.>=3. + company (str|unicode|iterable): The contact company. + NTT Docomo v.N/A. + + Examples: + >>> qrs = QrContact('Py Thon', email=('py@py.org', 'thon@py.org')) + >>> print(qrs) + QrContact + name: Py Thon + email: py@py.org + email: thon@py.org + >>> print(qrs.to_str()) + MECARD:N:Py Thon;EMAIL:py@py.org;EMAIL:thon@py.org;; + >>> qrs == QrContact.from_str(qrs.to_str()) + True + + >>> qrs = QrContact('QrSpecial', birthday=20160904) + >>> print(qrs) + QrContact + name: QrSpecial + birthday: 20160904 + >>> print(qrs.to_str()) + MECARD:N:QrSpecial;BDAY:20160904;; + + See Also: + https://en.wikipedia.org/wiki/MeCard + https://www.nttdocomo.co.jp/english/service/developer/make + /content/barcode/function/application/addressbook/index.html + https://zxingnet.codeplex.com/ + https://zxing.appspot.com/generator + """ + from dateutil.parser import parse + + if birthday: + birthday = str(int(birthday))[:8] + # QrSpecial.__init__(**locals()) # Py3-only + kws = locals() + kws.pop('self') + QrSpecial.__init__(self, **kws) + + +# ====================================================================== +class QrWifi(QrSpecial): + """ + QrSpecial-derived WiFi network. + """ + _start_tag = 'WIFI:' + _field_tags = dict(( + ('ssid', 'S'), + ('mode', 'T'), + ('password', 'P'), + ('hidden', 'H'), + )) + _modes = 'WEP', 'WPA', 'WPA2' + + # ================================== + def __init__( + self, + ssid=None, + mode=None, + password=None, + hidden=None): + """ + Generate the QrSpecial-derived WiFi network. + + This is useful + """ + if hidden: + hidden = 'true' + if mode: + mode = mode.upper() + if mode not in type(self)._modes: + mode = None + if password and not mode: + password = None + # QrSpecial.__init__(**locals()) # Py3-only + kws = locals() + kws.pop('self') + QrSpecial.__init__(self, **kws) + + +# ====================================================================== +# MeCard as an alias of QrContact +MeCard = QrContact + +# ====================================================================== +if __name__ == '__main__': + print(__doc__.strip()) + + # perform doctests + import doctest # Test interactive Python examples + + doctest.testmod() + + # qrs = MeCard('Riccardo Metere', email='rick@metere.it') + # print(qrs) + # print(qrs.to_str()) + # print(qrs.__dict__) + # print(QrContact.from_str(qrs.to_str()).__dict__) From de8c2b2abc4b51654c987dcfc13828599892d395 Mon Sep 17 00:00:00 2001 From: Riccardo M Date: Mon, 5 Sep 2016 00:59:35 +0200 Subject: [PATCH 2/5] FIX: completed docs. --- pyqrcode/qrspecial.py | 102 +++++++++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 25 deletions(-) diff --git a/pyqrcode/qrspecial.py b/pyqrcode/qrspecial.py index f883e45..46c7e89 100644 --- a/pyqrcode/qrspecial.py +++ b/pyqrcode/qrspecial.py @@ -36,9 +36,9 @@ from __future__ import print_function from __future__ import unicode_literals - # ====================================================================== # :: Python Standard Library Imports +import warnings # Warning control # ====================================================================== @@ -108,7 +108,6 @@ def __str__(self): lines = [ '{}: {}'.format(field, str(self.data[field])) for field in cls._fields if field in self.data] - text = cls.__name__ + '\n' + '\n'.join(lines) else: lines = [] for field in cls._fields: @@ -120,7 +119,12 @@ def __str__(self): else: lines.append('{}: {}'.format( field, str(self.data[field]))) - text = (cls.__name__ + '\n' + '\n'.join(lines)) + text = '<{}>'.format(cls.__name__) + if lines: + text += '\n' + '\n'.join(lines) + else: + text += ': ' + return text # ================================== @@ -287,6 +291,15 @@ def parse(text): Returns: obj (QrSpecial): The QrSpecial-derived object. + + Examples: + >>> print(QrSpecial.parse('tel:+39070653263')) + + number: +39070653263 + >>> print(QrSpecial.parse('mailto:spam@python.org')) + + email: spam@python.org + """ # Construct a QrSpecial subclss = ( @@ -323,7 +336,7 @@ def __init__( Examples: >>> qrs = QrPhone('+39070653263') >>> print(qrs) - QrPhone + number: +39070653263 >>> print(qrs.to_str()) tel:+39070653263 @@ -356,7 +369,7 @@ def __init__( Examples: >>> qrs = QrEmail('spam@python.org') >>> print(qrs) - QrEmail + email: spam@python.org >>> print(qrs.to_str()) mailto:spam@python.org @@ -392,7 +405,7 @@ def __init__( Examples: >>> qrs = QrMessage('+39070653263', 'I like your code!') >>> print(qrs) - QrMessage + number: +39070653263 text: I like your code! >>> print(qrs.to_str()) @@ -433,7 +446,7 @@ def __init__( Examples: >>> qrs = QrGeolocation(42.989, -71.465, 'www.python.org') >>> print(qrs) - QrGeolocation + lat: 42.989 lon: -71.465 query: www.python.org @@ -441,8 +454,9 @@ def __init__( geo:42.989,-71.465?q=www.python.org >>> qrs == QrGeolocation.from_str(qrs.to_str()) True + >>> print(QrGeolocation(47.68, -122.121)) - QrGeolocation + lat: 47.68 lon: -122.121 """ @@ -495,7 +509,7 @@ def __init__( Examples: >>> qrs = QrUrl('http://www.python.org') >>> print(qrs) - QrUrl + url: www.python.org >>> print(qrs.to_str()) http://www.python.org @@ -604,7 +618,7 @@ def __init__( Examples: >>> qrs = QrContact('Py Thon', email=('py@py.org', 'thon@py.org')) >>> print(qrs) - QrContact + name: Py Thon email: py@py.org email: thon@py.org @@ -615,7 +629,7 @@ def __init__( >>> qrs = QrContact('QrSpecial', birthday=20160904) >>> print(qrs) - QrContact + name: QrSpecial birthday: 20160904 >>> print(qrs.to_str()) @@ -646,7 +660,7 @@ class QrWifi(QrSpecial): _start_tag = 'WIFI:' _field_tags = dict(( ('ssid', 'S'), - ('mode', 'T'), + ('security', 'T'), ('password', 'P'), ('hidden', 'H'), )) @@ -656,21 +670,65 @@ class QrWifi(QrSpecial): def __init__( self, ssid=None, - mode=None, + security=None, password=None, hidden=None): """ Generate the QrSpecial-derived WiFi network. - This is useful + This is useful for sharing WiFi network authentication information. + + Args: + ssid (str|unicode): The SSID of the WiFi network. + security (str|unicode|None): The security standard to use. + Accepted values are: [WEP|WPA|WPA2] + Note that `WPA2` is not defined in the standard but it is + accepted by the ZXing implementation. + If None, no encryption is used. + password (str|unicode): The password of the WiFi network. + If security is None, it is forced to be empty. + hidden (bool): Determine the SSID broadcast mode. + If True, the SSID is not expected to be broadcast. + + Examples: + >>> qrs = QrWifi('Python', 'WEP', 'Monty', True) + >>> print(qrs) + + ssid: Python + security: WEP + password: Monty + hidden: true + >>> print(qrs.to_str()) + WIFI:S:Python;T:WEP;P:Monty;H:true;; + >>> qrs == QrWifi.from_str(qrs.to_str()) + True + + >>> qrs = QrWifi('Python', 'WEP', 'Monty') + >>> print(qrs) + + ssid: Python + security: WEP + password: Monty + >>> print(qrs.to_str()) + WIFI:S:Python;T:WEP;P:Monty;; + + >>> qrs = QrWifi('Python', 'X', 'Monty') + >>> print(qrs) + + ssid: Python + >>> print(qrs.to_str()) + WIFI:S:Python;; """ if hidden: hidden = 'true' - if mode: - mode = mode.upper() - if mode not in type(self)._modes: - mode = None - if password and not mode: + else: + hidden = None + if security: + security = security.upper() + if security not in type(self)._modes: + warnings.warn('Unknown WiFi security `{}`'.format(security)) + security = None + if password and not security: password = None # QrSpecial.__init__(**locals()) # Py3-only kws = locals() @@ -690,9 +748,3 @@ def __init__( import doctest # Test interactive Python examples doctest.testmod() - - # qrs = MeCard('Riccardo Metere', email='rick@metere.it') - # print(qrs) - # print(qrs.to_str()) - # print(qrs.__dict__) - # print(QrContact.from_str(qrs.to_str()).__dict__) From 2aee311542bede88ef5cf7ac331a61f2f94c2fc3 Mon Sep 17 00:00:00 2001 From: Riccardo M Date: Mon, 5 Sep 2016 08:55:26 +0200 Subject: [PATCH 3/5] FIX: improved URL handling and doc Fixed URL to support more protocols. The documentation should be fixed now. --- pyqrcode/qrspecial.py | 61 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/pyqrcode/qrspecial.py b/pyqrcode/qrspecial.py index 46c7e89..e88caee 100644 --- a/pyqrcode/qrspecial.py +++ b/pyqrcode/qrspecial.py @@ -492,33 +492,80 @@ def from_str(cls, text, strict=True, strip=True): # ====================================================================== class QrUrl(QrSpecial): """ - QrSpecial-derived URL. + QrSpecial-derived Uniform Resource Locator (URL). """ - _start_tag = 'http://' + _sep = '://' + _protocols = 'http', 'https', 'ftp', 'ftps' # ================================== def __init__( self, + protocol='http', url=None): """ - Generate the QrSpecial-derived e-mail address. + Generate the QrSpecial-derived Uniform Resource Locator (URL). + + Only the protocols requiring the '://' sequence of characters are + supported. Args: - url (str|unicode): The e-mail address. + protocol (str|unicode): The protocol of the URL. + It should be one of: [http|https|ftp|ftps]. + Otherwise a warning is issued. + url (str|unicode): The Uniform Resource Locator (URL). + If the protocol is specified directly in the URL, the + `protocol` keyword argument is ignored. Examples: - >>> qrs = QrUrl('http://www.python.org') + >>> qrs = QrUrl(url='https://www.python.org') >>> print(qrs) + protocol: https + url: www.python.org + >>> print(qrs.to_str()) + https://www.python.org + >>> qrs == QrUrl.from_str(qrs.to_str()) + True + + >>> qrs = QrUrl(url='www.python.org') + >>> print(qrs) + + protocol: http url: www.python.org >>> print(qrs.to_str()) http://www.python.org >>> qrs == QrUrl.from_str(qrs.to_str()) True + + >>> qrs = QrUrl('https', 'www.python.org') + >>> print(qrs) + + protocol: https + url: www.python.org + >>> print(qrs.to_str()) + https://www.python.org + >>> qrs == QrUrl.from_str(qrs.to_str()) + True + + >>> with warnings.catch_warnings(record=True) as w: + ... qrs = QrUrl(url='mtp://www.python.org') + ... assert(len(w) == 1) + ... 'Unusual protocol detected' in str(w[-1].message) + True + >>> print(qrs) + + protocol: mtp + url: www.python.org + >>> print(qrs.to_str()) + mtp://www.python.org """ cls = type(self) - if url is not None and url.startswith(cls._start_tag): - url = url[len(cls._start_tag):] + if url: + parsed = url.split(cls._sep, 1) + if len(parsed) > 1: + protocol, url = parsed + if protocol not in cls._protocols: + warnings.warn('Unusual protocol detected `{}`'.format(protocol)) # QrSpecial.__init__(**locals()) # Py3-only kws = locals() kws.pop('self') From 9fc4b05f8f300ff95ef914290bb89c492497dff8 Mon Sep 17 00:00:00 2001 From: Riccardo M Date: Mon, 5 Sep 2016 15:46:15 +0200 Subject: [PATCH 4/5] FIX: changed QrUrl .__init__ signature inverted 'url' and 'protocol' in QrUrl.__init__ signature --- pyqrcode/qrspecial.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pyqrcode/qrspecial.py b/pyqrcode/qrspecial.py index e88caee..9012b08 100644 --- a/pyqrcode/qrspecial.py +++ b/pyqrcode/qrspecial.py @@ -83,8 +83,9 @@ def __init__(self, **kws): cls._is_simple = len(cls._field_tags) == 0 # cls._fields = signature(cls).parameters.keys() # Py3-only # first argument is always `self` - cls._fields = cls.__init__.__code__.co_varnames[ - 1:cls.__init__.__code__.co_argcount] + if not cls._fields: + cls._fields = cls.__init__.__code__.co_varnames[ + 1:cls.__init__.__code__.co_argcount] self.data = {} for field in cls._fields: if field in kws: @@ -496,15 +497,18 @@ class QrUrl(QrSpecial): """ _sep = '://' _protocols = 'http', 'https', 'ftp', 'ftps' + _fields = ('protocol', 'url') # ================================== def __init__( self, - protocol='http', - url=None): + url=None, + protocol='http'): """ Generate the QrSpecial-derived Uniform Resource Locator (URL). + This assumes that the HTTP protocol will be used. + Only the protocols requiring the '://' sequence of characters are supported. @@ -517,7 +521,7 @@ def __init__( `protocol` keyword argument is ignored. Examples: - >>> qrs = QrUrl(url='https://www.python.org') + >>> qrs = QrUrl('https://www.python.org') >>> print(qrs) protocol: https @@ -527,7 +531,7 @@ def __init__( >>> qrs == QrUrl.from_str(qrs.to_str()) True - >>> qrs = QrUrl(url='www.python.org') + >>> qrs = QrUrl('www.python.org') >>> print(qrs) protocol: http @@ -537,7 +541,7 @@ def __init__( >>> qrs == QrUrl.from_str(qrs.to_str()) True - >>> qrs = QrUrl('https', 'www.python.org') + >>> qrs = QrUrl('www.python.org', 'https') >>> print(qrs) protocol: https @@ -759,7 +763,11 @@ def __init__( >>> print(qrs.to_str()) WIFI:S:Python;T:WEP;P:Monty;; - >>> qrs = QrWifi('Python', 'X', 'Monty') + >>> with warnings.catch_warnings(record=True) as w: + ... qrs = QrWifi('Python', 'X', 'Monty') + ... assert(len(w) == 1) + ... 'Unknown WiFi security' in str(w[-1].message) + True >>> print(qrs) ssid: Python From 8b4cb3ada063ab8cbffd820b63089851584d68eb Mon Sep 17 00:00:00 2001 From: Riccardo M Date: Mon, 5 Sep 2016 16:15:50 +0200 Subject: [PATCH 5/5] FIX: updated docs --- pyqrcode/qrspecial.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqrcode/qrspecial.py b/pyqrcode/qrspecial.py index 9012b08..bc45e73 100644 --- a/pyqrcode/qrspecial.py +++ b/pyqrcode/qrspecial.py @@ -513,12 +513,12 @@ def __init__( supported. Args: - protocol (str|unicode): The protocol of the URL. - It should be one of: [http|https|ftp|ftps]. - Otherwise a warning is issued. url (str|unicode): The Uniform Resource Locator (URL). If the protocol is specified directly in the URL, the `protocol` keyword argument is ignored. + protocol (str|unicode): The protocol of the URL. + It should be one of: [http|https|ftp|ftps]. + Otherwise a warning is issued. Examples: >>> qrs = QrUrl('https://www.python.org')