Skip to content

Commit

Permalink
homebrew: Move repeated logic from homebrew modules into module_utils (
Browse files Browse the repository at this point in the history
…#8324)

* gomebrew: Move repeated logic from homebrew modules into module_utils

Fixes #8323.

* ghangelog + unit test improvement

* Update changelogs/fragments/8323-refactor-homebrew-logic-module-utils.yml

Co-authored-by: Felix Fontein <[email protected]>

---------

Co-authored-by: Felix Fontein <[email protected]>
  • Loading branch information
kitizz and felixfontein authored May 11, 2024
1 parent 136419c commit 3b7f13c
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 177 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- "homebrew, homebrew_cask - refactor common argument validation logic into a dedicated ``homebrew`` module utils (https://github.com/ansible-collections/community.general/issues/8323, https://github.com/ansible-collections/community.general/pull/8324)."
115 changes: 115 additions & 0 deletions plugins/module_utils/homebrew.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
# Copyright (c) Ansible project
# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause)
# SPDX-License-Identifier: BSD-2-Clause

from __future__ import absolute_import, division, print_function

__metaclass__ = type

import os
import re
from ansible.module_utils.six import string_types


def _create_regex_group_complement(s):
lines = (line.strip() for line in s.split("\n") if line.strip())
chars = filter(None, (line.split("#")[0].strip() for line in lines))
group = r"[^" + r"".join(chars) + r"]"
return re.compile(group)


class HomebrewValidate(object):
# class regexes ------------------------------------------------ {{{
VALID_PATH_CHARS = r"""
\w # alphanumeric characters (i.e., [a-zA-Z0-9_])
\s # spaces
: # colons
{sep} # the OS-specific path separator
. # dots
\- # dashes
""".format(
sep=os.path.sep
)

VALID_BREW_PATH_CHARS = r"""
\w # alphanumeric characters (i.e., [a-zA-Z0-9_])
\s # spaces
{sep} # the OS-specific path separator
. # dots
\- # dashes
""".format(
sep=os.path.sep
)

VALID_PACKAGE_CHARS = r"""
\w # alphanumeric characters (i.e., [a-zA-Z0-9_])
. # dots
/ # slash (for taps)
\+ # plusses
\- # dashes
: # colons (for URLs)
@ # at-sign
"""

INVALID_PATH_REGEX = _create_regex_group_complement(VALID_PATH_CHARS)
INVALID_BREW_PATH_REGEX = _create_regex_group_complement(VALID_BREW_PATH_CHARS)
INVALID_PACKAGE_REGEX = _create_regex_group_complement(VALID_PACKAGE_CHARS)
# /class regexes ----------------------------------------------- }}}

# class validations -------------------------------------------- {{{
@classmethod
def valid_path(cls, path):
"""
`path` must be one of:
- list of paths
- a string containing only:
- alphanumeric characters
- dashes
- dots
- spaces
- colons
- os.path.sep
"""

if isinstance(path, string_types):
return not cls.INVALID_PATH_REGEX.search(path)

try:
iter(path)
except TypeError:
return False
else:
paths = path
return all(cls.valid_brew_path(path_) for path_ in paths)

@classmethod
def valid_brew_path(cls, brew_path):
"""
`brew_path` must be one of:
- None
- a string containing only:
- alphanumeric characters
- dashes
- dots
- spaces
- os.path.sep
"""

if brew_path is None:
return True

return isinstance(
brew_path, string_types
) and not cls.INVALID_BREW_PATH_REGEX.search(brew_path)

@classmethod
def valid_package(cls, package):
"""A valid package is either None or alphanumeric."""

if package is None:
return True

return isinstance(
package, string_types
) and not cls.INVALID_PACKAGE_REGEX.search(package)
116 changes: 13 additions & 103 deletions plugins/modules/homebrew.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,10 @@
'''

import json
import os.path
import re

from ansible_collections.community.general.plugins.module_utils.homebrew import HomebrewValidate

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems, string_types

Expand All @@ -208,98 +209,7 @@ def _check_package_in_json(json_output, package_type):
class Homebrew(object):
'''A class to manage Homebrew packages.'''

# class regexes ------------------------------------------------ {{{
VALID_PATH_CHARS = r'''
\w # alphanumeric characters (i.e., [a-zA-Z0-9_])
\s # spaces
: # colons
{sep} # the OS-specific path separator
. # dots
\- # dashes
'''.format(sep=os.path.sep)

VALID_BREW_PATH_CHARS = r'''
\w # alphanumeric characters (i.e., [a-zA-Z0-9_])
\s # spaces
{sep} # the OS-specific path separator
. # dots
\- # dashes
'''.format(sep=os.path.sep)

VALID_PACKAGE_CHARS = r'''
\w # alphanumeric characters (i.e., [a-zA-Z0-9_])
. # dots
/ # slash (for taps)
\+ # plusses
\- # dashes
: # colons (for URLs)
@ # at-sign
'''

INVALID_PATH_REGEX = _create_regex_group_complement(VALID_PATH_CHARS)
INVALID_BREW_PATH_REGEX = _create_regex_group_complement(VALID_BREW_PATH_CHARS)
INVALID_PACKAGE_REGEX = _create_regex_group_complement(VALID_PACKAGE_CHARS)
# /class regexes ----------------------------------------------- }}}

# class validations -------------------------------------------- {{{
@classmethod
def valid_path(cls, path):
'''
`path` must be one of:
- list of paths
- a string containing only:
- alphanumeric characters
- dashes
- dots
- spaces
- colons
- os.path.sep
'''

if isinstance(path, string_types):
return not cls.INVALID_PATH_REGEX.search(path)

try:
iter(path)
except TypeError:
return False
else:
paths = path
return all(cls.valid_brew_path(path_) for path_ in paths)

@classmethod
def valid_brew_path(cls, brew_path):
'''
`brew_path` must be one of:
- None
- a string containing only:
- alphanumeric characters
- dashes
- dots
- spaces
- os.path.sep
'''

if brew_path is None:
return True

return (
isinstance(brew_path, string_types)
and not cls.INVALID_BREW_PATH_REGEX.search(brew_path)
)

@classmethod
def valid_package(cls, package):
'''A valid package is either None or alphanumeric.'''

if package is None:
return True

return (
isinstance(package, string_types)
and not cls.INVALID_PACKAGE_REGEX.search(package)
)

@classmethod
def valid_state(cls, state):
'''
Expand Down Expand Up @@ -359,7 +269,7 @@ def path(self):

@path.setter
def path(self, path):
if not self.valid_path(path):
if not HomebrewValidate.valid_path(path):
self._path = []
self.failed = True
self.message = 'Invalid path: {0}.'.format(path)
Expand All @@ -379,7 +289,7 @@ def brew_path(self):

@brew_path.setter
def brew_path(self, brew_path):
if not self.valid_brew_path(brew_path):
if not HomebrewValidate.valid_brew_path(brew_path):
self._brew_path = None
self.failed = True
self.message = 'Invalid brew_path: {0}.'.format(brew_path)
Expand All @@ -404,7 +314,7 @@ def current_package(self):

@current_package.setter
def current_package(self, package):
if not self.valid_package(package):
if not HomebrewValidate.valid_package(package):
self._current_package = None
self.failed = True
self.message = 'Invalid package: {0}.'.format(package)
Expand Down Expand Up @@ -491,7 +401,7 @@ def run(self):

# checks ------------------------------------------------------- {{{
def _current_package_is_installed(self):
if not self.valid_package(self.current_package):
if not HomebrewValidate.valid_package(self.current_package):
self.failed = True
self.message = 'Invalid package: {0}.'.format(self.current_package)
raise HomebrewException(self.message)
Expand All @@ -514,7 +424,7 @@ def _current_package_is_installed(self):
return _check_package_in_json(data, "formulae") or _check_package_in_json(data, "casks")

def _current_package_is_outdated(self):
if not self.valid_package(self.current_package):
if not HomebrewValidate.valid_package(self.current_package):
return False

rc, out, err = self.module.run_command([
Expand All @@ -526,7 +436,7 @@ def _current_package_is_outdated(self):
return rc != 0

def _current_package_is_installed_from_head(self):
if not Homebrew.valid_package(self.current_package):
if not HomebrewValidate.valid_package(self.current_package):
return False
elif not self._current_package_is_installed():
return False
Expand Down Expand Up @@ -624,7 +534,7 @@ def _upgrade_all(self):

# installed ------------------------------ {{{
def _install_current_package(self):
if not self.valid_package(self.current_package):
if not HomebrewValidate.valid_package(self.current_package):
self.failed = True
self.message = 'Invalid package: {0}.'.format(self.current_package)
raise HomebrewException(self.message)
Expand Down Expand Up @@ -685,7 +595,7 @@ def _install_packages(self):
def _upgrade_current_package(self):
command = 'upgrade'

if not self.valid_package(self.current_package):
if not HomebrewValidate.valid_package(self.current_package):
self.failed = True
self.message = 'Invalid package: {0}.'.format(self.current_package)
raise HomebrewException(self.message)
Expand Down Expand Up @@ -756,7 +666,7 @@ def _upgrade_packages(self):

# uninstalled ---------------------------- {{{
def _uninstall_current_package(self):
if not self.valid_package(self.current_package):
if not HomebrewValidate.valid_package(self.current_package):
self.failed = True
self.message = 'Invalid package: {0}.'.format(self.current_package)
raise HomebrewException(self.message)
Expand Down Expand Up @@ -805,7 +715,7 @@ def _uninstall_packages(self):

# linked --------------------------------- {{{
def _link_current_package(self):
if not self.valid_package(self.current_package):
if not HomebrewValidate.valid_package(self.current_package):
self.failed = True
self.message = 'Invalid package: {0}.'.format(self.current_package)
raise HomebrewException(self.message)
Expand Down Expand Up @@ -852,7 +762,7 @@ def _link_packages(self):

# unlinked ------------------------------- {{{
def _unlink_current_package(self):
if not self.valid_package(self.current_package):
if not HomebrewValidate.valid_package(self.current_package):
self.failed = True
self.message = 'Invalid package: {0}.'.format(self.current_package)
raise HomebrewException(self.message)
Expand Down
Loading

0 comments on commit 3b7f13c

Please sign in to comment.