Source code for superdjango.utils.library

# Imports

from bs4 import BeautifulSoup
import csv
import io
import logging
import os
import re
from shutil import copy2
import six
from .constants import BASE10, BASE62, BOOLEAN_VALUES, FALSE_VALUES, TRUE_VALUES

log = logging.getLogger(__name__)

# Exports

__all__ = (
    "append_file",
    "average",
    "base_convert",
    "camelcase_to_underscore",
    "copy_file",
    "copy_tree",
    "indent",
    "is_bool",
    "is_integer",
    "is_string",
    "percentage",
    "read_csv",
    "read_file",
    "smart_cast",
    "strip_html_tags",
    "to_bool",
    "truncate",
    "underscore_to_camelcase",
    "underscore_to_title_case",
    "write_file",
    "File",
)

# Functions


[docs]def append_file(path, content): """Append to a file. :param path: The path to the file. :type path: str :param content: The content to be appended the file. :type content: str .. code-block:: python from superdjango.utils import append_file append_file("path/to/readme.txt", "This is a test.") """ with io.open(path, "a", encoding="utf-8") as f: f.write(content) f.close()
[docs]def average(values): """Calculate the average of a given number of values. :param values: The values to be averaged. :type values: list | tuple :rtype: float Ever get tired of creating a try/except for zero division? I do. .. code-block:: python from superdjango.utils import average values = [1, 2, 3, 4, 5] print(average(values)) """ try: return float(sum(values) / len(values)) except ZeroDivisionError: return 0.0
[docs]def base_convert(number, from_digits=BASE10, to_digits=BASE62): """Convert a number between two bases of arbitrary digits. :param number: The number to be converted. :type number: int :param from_digits: The digits to use as the source of the conversion. ``number`` is included in these digits. :type from_digits: str :param to_digits: The digits to which the number will be converted. :type to_digits: str :rtype: str """ if str(number)[0] == '-': number = str(number)[1:] negative = True else: negative = False x = 0 for digit in str(number): x = x * len(from_digits) + from_digits.index(digit) if x == 0: result = to_digits[0] else: result = "" while x > 0: digit = x % len(to_digits) result = to_digits[digit] + result x = int(x / len(to_digits)) if negative: result = "-" + result return result
[docs]def camelcase_to_underscore(string): """Convert a given string from ``CamelCase`` to ``camel_case``. :param string: The string to be converted. :type string: str :rtype: str """ # http://djangosnippets.org/snippets/585/ return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', string).lower().strip('_')
[docs]def copy_file(from_path, to_path, make_directories=False): """Copy a file from one location to another. :param from_path: The source path. :type from_path: str || unicode :param to_path: The destination path. :type to_path: str || unicode :param make_directories: Create directories as needed along the ``to_path``. :type make_directories: bool :rtype: tuple(bool, str) :returns: Success or failure and a message if failure. .. code-block:: python from superdjango.utils import copy_file copy_file("readme-template.txt", "path/to/project/readme.txt") """ if make_directories: base_path = os.path.dirname(to_path) if not os.path.exists(base_path): os.makedirs(base_path) try: copy2(from_path, to_path) return True, None except IOError as e: return False, str(e)
[docs]def copy_tree(from_path, to_path): """Recursively copy a source directory to a given destination. :param from_path: The source directory. :type from_path: str :param to_path: The destination directory. This must already exist. :type to_path: str :rtype: bool :returns: ``True`` if successful. .. note:: Errors are logged using the Python log. .. code-block:: python from superdjango.utils import copy_tree success = copy_tree("from/path", "to/path") print(success) """ # Deal with absolutes and user expansion. source = os.path.abspath(os.path.expanduser(from_path)) destination = os.path.abspath(os.path.expanduser(to_path)) if not os.path.exists(destination): log.error("Destination does not exist: %s" % destination) return False # Iterate through the source. success = True for root, dirs, files in os.walk(source): directory_path = os.path.join(destination, os.path.relpath(root, source)) if not os.path.exists(directory_path): os.mkdir(directory_path) for f in files: source_file = os.path.join(root, f) file_path = os.path.join(directory_path, f) try: copy2(source_file, file_path) except IOError as e: success = False log.warning("Could not copy %s: %s" % (source_file, e)) return success
[docs]def indent(text, amount=4): """Indent a string. :param text: The text to be indented. :type text: str :param amount: The number of spaces to use for indentation. :type amount: int :rtype: str .. code-block:: python from superdjango.utils import indent text = "This text will be indented." print(indent(text)) """ prefix = " " * amount return prefix + text.replace('\n', '\n' + prefix)
[docs]def is_bool(value, test_values=BOOLEAN_VALUES): """Determine if the given value is a boolean at run time. :param value: The value to be checked. :param test_values: The possible values that could be True or False. :type test_values: list | tuple :rtype: bool .. code-block:: python from superdjango.utils import is_bool print(is_bool("yes")) print(is_bool(True)) print(is_bool("No")) print(is_bool(False)) .. note:: By default, a liberal number of values are used to test. If you *just* want ``True`` or ``False``, simply pass ``(True, False)`` as ``test_values``. """ return value in test_values
[docs]def is_integer(value, cast=False): """Indicates whether the given value is an integer. Saves a little typing. :param value: The value to be checked. :param cast: Indicates whether the value (when given as a string) should be cast to an integer. :type cast: bool :rtype: bool .. code-block:: python from superdjango.utils import is_integer print(is_integer(17)) print(is_integer(17.5)) print(is_integer("17")) print(is_integer("17", cast=True)) """ if isinstance(value, int): return True if isinstance(value, (float, str)) and cast: try: int(value) except ValueError: return False else: return True return False
[docs]def is_string(value): """Indicates whether the given value is a string. Saves a little typing. :param value: The value to be checked. :rtype: bool .. code-block:: python from superdjango.utils import is_string print(is_string("testing")) print(is_string("17")) print(is_string(17)) """ return isinstance(value, six.string_types)
# def match_all(*conditions, lazy=False): # """Evaluate all provided conditions. # # :param conditions: A list of boolean conditions. # # :param lazy: Indicates that each condition may be evaluated separately, but all must be ``True``. This is useful # when one or more conditions are dependent upon previous conditions. # # :rtype: bool # :returns: ``True`` if all conditions are ``True``. # # """ # if not lazy: # return all(conditions) # # results = list() # for c in conditions: # if c == True: # results.append(True) # else: # results.append(False) # # return all(results) # # # def match_any(*conditions, lazy=False): # """Evaluate any provided conditions. # # :param conditions: A list of boolean conditions. # # :param lazy: Indicates that each condition may be evaluated separately, but at least one must be ``True``. This is # useful when one or more conditions are dependent upon previous conditions. # # :rtype: bool # :returns: ``True`` if any conditions are ``True``. # # """ # if not lazy: # return any(conditions) # # results = list() # for c in conditions: # if c == True: # results.append(True) # else: # results.append(False) # # return any(results)
[docs]def percentage(portion, total): """Calculate the percentage that a portion makes up of a total. :param portion: The portion of the total to be calculated as a percentage. :type portion: float | int :param total: The total amount. :type total: float | int :rtype: float .. code-block:: python from superdjango.utils import percentage p = percentage(50, 100) print(p + "%") """ try: return 100.0 * portion / total except (TypeError, ZeroDivisionError): return 0.0
[docs]def read_csv(path, encoding="utf-8", first_row_field_names=False): """Read the contents of a CSV file. :param path: The path to the file. :type path: str :param encoding: The encoding of the file. :type encoding: str :param first_row_field_names: Indicates the first row contains the field names. In this case the returned rows will be a dictionary rather than a list. :type first_row_field_names: bool :rtype: list[list] || list[dict] .. code-block:: text menu,identifier,sort_order,text,url main,product,10,Product,/product/ main,solutions,20,Solutions,/solutions/ main,resources,30,Resources,/resources/ main,support,40,Support,https://support.example.com main,about,50,About,/about/ main,contact,60,Contact,/contact/ .. code-block:: python from superdjango.utils import read_csv rows = read_csv("path/to/menus.csv", first_row_fields_names=True) for r in rows: print("%s: %s" % (row['identifier'], row['url'] """ with io.open(path, "r", encoding=encoding) as f: if first_row_field_names: reader = csv.DictReader(f) else: reader = csv.reader(f) rows = list() for row in reader: rows.append(row) f.close() return rows
[docs]def read_file(path): """Read a file and return its contents. :param path: The path to the file. :type path: str || unicode :rtype: str .. code-block:: python from superdjango.utils import read_file output = read_file("path/to/readme.txt") print(output) """ with io.open(path, "r", encoding="utf-8") as f: output = f.read() f.close() return output
[docs]def smart_cast(value): """Intelligently cast the given value to a Python data type. :param value: The value to be cast. :type value: str """ # Handle integers first because is_bool() may interpret 0s and 1s ad booleans. if is_integer(value, cast=True): return int(value) elif is_bool(value): return to_bool(value) else: return value
[docs]def strip_html_tags(html): """Strip HTML tags from a string. :param html: The string from which HTML tags should be stripped. :type html: str | unicode :rtype: str .. code-block:: python from superdjango.utils import strip_html_tags html = "<p>This string contains <b>HTML</b> tags.</p>" print(strip_html_tags(html)) """ return "".join(BeautifulSoup(html, "html.parser").find_all(text=True))
[docs]def to_bool(value, false_values=FALSE_VALUES, true_values=TRUE_VALUES): """Convert the given value to it's boolean equivalent. :param value: The value to be converted. :param false_values: The possible values that could be False. :type false_values: list | tuple :param true_values: The possible values that could be True. :type true_values: list | tuple :rtype: bool :raises: ``ValueError`` if the value could not be converted. .. code-block:: python from superdjango.utils import to_bool print(to_bool("yes")) print(to_bool(1)) print(to_bool("no")) print(to_bool(0)) """ if value in true_values: return True if value in false_values: return False raise ValueError('"%s" cannot be converted to True or False.')
[docs]def truncate(string, continuation="...", limit=30): """Get a truncated version of a string if if over the limit. :param string: The string to be truncated. :type string: str || None :param limit: The maximum number of characters. :type limit: int :param continuation: The string to add to the truncated title. :type continuation: str || None :rtype: str .. code-block:: python from superdjango.utils import truncate title = "This Title is Too Long to Be Displayed As Is" print(truncate(title)) """ # Make it safe to submit the string as None. if string is None: return "" # There's nothing to do if the string is not over the limit. if len(string) <= limit: return string # Adjust the limit according to the string length, otherwise we'll still be over. if continuation: limit -= len(continuation) # Return the altered title. if continuation: return string[:limit] + continuation else: return string[:limit]
[docs]def underscore_to_camelcase(string): """Convert a string with underscore separations to CamelCase. :param string: The string to be converted. :type string: str :rtype: str """ return string.replace("_", " ").title().replace(" ", "")
[docs]def underscore_to_title_case(string): """Convert a string to title case. :param string: The string to be converted. :type string: str :rtype: str """ return string.replace("_", " ").title()
[docs]def write_file(path, content="", make_directories=False): """Write a file. :param path: The path to the file. :type path: str || unicode :param content: The content of the file. An empty string is effectively the same as a "touch". :type content: str || unicode :param make_directories: Create directories as needed along the file path. :type make_directories: bool .. code-block:: python from superdjango.utils import write_file write_file("path/to/readme.txt", "This is a test.") """ if make_directories: base_path = os.path.dirname(path) if not os.path.exists(base_path): os.makedirs(base_path) with io.open(path, "w", encoding="utf-8") as f: f.write(content) f.close()
# Classes
[docs]class File(object): """A simple helper class for working with file names. For more robust handling of paths, see `pathlib`_. .. _pathlib: https://docs.python.org/3/library/pathlib.html """
[docs] def __init__(self, path): """Initialize the file instance. :param path: The path to the file. :type path: str """ self.basename = os.path.basename(path) self.directory = os.path.dirname(path) self.extension = os.path.splitext(path)[-1] self.name = os.path.basename(os.path.splitext(path)[0]) self.path = path
def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.basename) @property def exists(self): """Indicates the file exists. :rtype: bool """ return os.path.exists(self.path)