From 42267b5ec3d339a013f873bcb3d12f942e734d2e Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jul 04 2018 06:29:55 +0000
Subject: [PATCH 1/2] Modularize package metadata API


It is possible to access package metadata indepently.

    from pyrpkg.pkginfo import PackageMetadata
    from pyrpkg.pkgrepo import PackageRepository
    repo = PackageRepository('/path/to/repo')
    pkg = PackageMetadata(repo)
    print(pkg.nvr)
    print(pkg.spec)
    print(pkg.target)

Fixes #2

Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/pyrpkg/__init__.py b/pyrpkg/__init__.py
index c05b753..1448dbb 100644
--- a/pyrpkg/__init__.py
+++ b/pyrpkg/__init__.py
@@ -20,7 +20,6 @@ import io
 import koji
 import logging
 import os
-import posixpath
 import re
 import rpm
 import shutil
@@ -42,6 +41,7 @@ from pyrpkg.errors import HashtypeMixingError, rpkgError, rpkgAuthError, \
 from .gitignore import GitIgnore
 from pyrpkg.lookaside import CGILookasideCache
 from pyrpkg.sources import SourcesFile
+from pyrpkg.pkginfo import PackageMetadata
 from pyrpkg.pkgrepo import PackageRepository
 from pyrpkg.utils import cached_property, log_result, find_me
 
@@ -119,42 +119,12 @@ class Commands(object):
         self._anon_kojisession = None
         # The latest commit
         self._commit = None
-        # The disttag rpm value
-        self._disttag = None
-        # The distval rpm value
-        self._distval = None
-        # The distvar rpm value
-        self._distvar = None
-        # The rpm epoch of the cloned package
-        self._epoch = None
         # An authenticated buildsys session
         self._kojisession = None
         # A web url of the buildsys server
         self._kojiweburl = None
-        # The local arch to use in rpm building
-        self._localarch = None
-        # A property to load the mock config
-        self._mockconfig = None
-        # The name of the cloned repository
-        self._module_name = None
-        # The dist git namespace
-        self._ns = None
-        # The package name from spec file
-        self._package_name_spec = None
-        # The rpm name-version-release of the cloned package
-        self._nvr = None
-        # The rpm release of the cloned package
-        self._rel = None
         # The cloned repo object
         self._repo = None
-        # The rpm defines used when calling rpm
-        self._rpmdefines = None
-        # The specfile in the cloned package
-        self._spec = None
-        # The build target within the buildsystem
-        self._target = target
-        # The build target for containers within the buildsystem
-        self._container_build_target = target
         # The top url to our build server
         self._topurl = None
         # The user to use or discover
@@ -163,8 +133,6 @@ class Commands(object):
         self._password = None
         # The alternate Koji user to run commands as
         self._runas = None
-        # The rpm version of the cloned package
-        self._ver = None
         self.log = log
         # Default sources file output format type
         self.source_entry_type = 'old'
@@ -182,30 +150,6 @@ class Commands(object):
         # package name will be sent to lookaside CGI script as 'namespace/name'
         # instead of just name.
         self.lookaside_namespaced = lookaside_namespaced
-        # Deprecates self.module_name
-        self._repo_name = None
-
-        if distgit_namespaced:
-            try:
-                repo_name = self.repo.push_url
-            except rpkgError:
-                # Ignore error if cannot get remote push URL from this repo.
-                # That is we just skip has_namespace check when that error
-                # happens.
-                pass
-            else:
-                parts = urllib.parse.urlparse(repo_name)
-                parts = [p for p in parts.path.split('/') if p]
-                not_contain_namespace = len(parts) == 1
-                if not_contain_namespace:
-                    self.log.warning(
-                        'Your git configuration does not use a namespace.')
-                    self.log.warning(
-                        'Consider updating your git configuration by running:')
-                    self.log.warning(
-                        '  git remote set-url %s %s',
-                        self.repo.branch_remote,
-                        self._get_namespace_giturl(parts[0]))
 
     # Define properties here
     # Properties allow us to "lazy load" various attributes, which also means
@@ -408,38 +352,6 @@ class Commands(object):
             self.login_koji_session(koji_config, self._kojisession)
 
     @property
-    def disttag(self):
-        """This property ensures the disttag attribute"""
-
-        if not self._disttag:
-            self.load_rpmdefines()
-        return self._disttag
-
-    @property
-    def distval(self):
-        """This property ensures the distval attribute"""
-
-        if not self._distval:
-            self.load_rpmdefines()
-        return self._distval
-
-    @property
-    def distvar(self):
-        """This property ensures the distvar attribute"""
-
-        if not self._distvar:
-            self.load_rpmdefines()
-        return self._distvar
-
-    @property
-    def epoch(self):
-        """This property ensures the epoch attribute"""
-
-        if not self._epoch:
-            self.load_nameverrel()
-        return self._epoch
-
-    @property
     def kojiweburl(self):
         """This property ensures the kojiweburl attribute"""
 
@@ -448,208 +360,6 @@ class Commands(object):
         return self._kojiweburl
 
     @property
-    def localarch(self):
-        """This property ensures the localarch attribute"""
-
-        if not self._localarch:
-            self.load_localarch()
-        return(self._localarch)
-
-    def load_localarch(self):
-        """Get the local arch as defined by rpm"""
-
-        proc = subprocess.Popen(['rpm --eval %{_arch}'], shell=True,
-                                stdout=subprocess.PIPE,
-                                universal_newlines=True)
-        self._localarch = proc.communicate()[0].strip('\n')
-
-    @property
-    def mockconfig(self):
-        """This property ensures the mockconfig attribute"""
-
-        if not self._mockconfig:
-            self.load_mockconfig()
-        return self._mockconfig
-
-    @mockconfig.setter
-    def mockconfig(self, config):
-        self._mockconfig = config
-
-    def load_mockconfig(self):
-        """This sets the mockconfig attribute"""
-
-        self._mockconfig = '%s-%s' % (self.target, self.localarch)
-
-    @property
-    def repo_name(self):
-        """Property to get repository name"""
-        if not self._repo_name:
-            self.load_repo_name()
-        return self._repo_name
-
-    @repo_name.setter
-    def repo_name(self, name):
-        """Set repository name"""
-        self._repo_name = name
-
-    @property
-    def module_name(self):
-        """This property ensures the module attribute"""
-        self.log.warning('Property module_name is deprecated. Please use '
-                         'repo_name instead.')
-        return self.repo_name
-
-    @module_name.setter
-    def module_name(self, module_name):
-        self.log.warning('Property module_name is deprecated. Please use '
-                         'repo_name instead.')
-        self.repo_name = module_name
-
-    def load_module_name(self):
-        return self.load_repo_name()
-
-    def load_repo_name(self):
-        """Loads repository name"""
-
-        try:
-            if self.repo.push_url:
-                parts = urllib.parse.urlparse(self.repo.push_url)
-
-                # FIXME
-                # if self.distgit_namespaced:
-                #     self._module_name = "/".join(parts.path.split("/")[-2:])
-                repo_name = posixpath.basename(parts.path.strip('/'))
-
-                if repo_name.endswith('.git'):
-                    repo_name = repo_name[:-len('.git')]
-                self._repo_name = repo_name
-                return
-        except rpkgError:
-            self.log.warning('Failed to get repository name from Git url or pushurl')
-
-        self.load_nameverrel()
-        if self._package_name_spec:
-            self._repo_name = self._package_name_spec
-            return
-
-        raise rpkgError('Could not find current repository name.'
-                        ' Use --name and optional --namespace.')
-
-    @property
-    def ns(self):
-        """This property provides the namespace of the repository"""
-
-        if not self._ns:
-            self.load_ns()
-        return self._ns
-
-    @ns.setter
-    def ns(self, ns):
-        self._ns = ns
-
-    def load_ns(self):
-        """Loads the namespace"""
-
-        try:
-            if self.distgit_namespaced:
-                if self.repo.push_url:
-                    parts = urllib.parse.urlparse(self.repo.push_url)
-
-                    path_parts = [p for p in parts.path.split("/") if p]
-                    if len(path_parts) == 1:
-                        path_parts.insert(0, "rpms")
-                    ns = path_parts[-2]
-
-                    self._ns = ns
-            else:
-                self._ns = None
-                self.log.info("Could not find ns, distgit is not namespaced")
-        except rpkgError:
-            self.log.warning('Failed to get ns from Git url or pushurl')
-
-    @property
-    def ns_module_name(self):
-        self.log.warning('Property ns_module_name is deprecated. Please use '
-                         'ns_repo_name instead.')
-        return self.ns_repo_name
-
-    @property
-    def ns_repo_name(self):
-        if self.distgit_namespaced:
-            return '%s/%s' % (self.ns, self.repo_name)
-        else:
-            return self.repo_name
-
-    @property
-    def nvr(self):
-        """This property ensures the nvr attribute"""
-
-        if not self._nvr:
-            self.load_nvr()
-        return self._nvr
-
-    def load_nvr(self):
-        """This sets the nvr attribute"""
-
-        self._nvr = '%s-%s-%s' % (self.repo_name, self.ver, self.rel)
-
-    @property
-    def rel(self):
-        """This property ensures the rel attribute"""
-        if not self._rel:
-            self.load_nameverrel()
-        return(self._rel)
-
-    def load_nameverrel(self):
-        """Set the release of a package."""
-
-        cmd = ['rpm']
-        cmd.extend(self.rpmdefines)
-        # We make sure there is a space at the end of our query so that
-        # we can split it later.  When there are subpackages, we get a
-        # listing for each subpackage.  We only care about the first.
-        cmd.extend(['-q', '--qf', '"??%{NAME} %{EPOCH} %{VERSION} %{RELEASE}??"',
-                    '--specfile', '"%s"' % os.path.join(self.path, self.spec)])
-        joined_cmd = ' '.join(cmd)
-        try:
-            proc = subprocess.Popen(joined_cmd, shell=True,
-                                    universal_newlines=True,
-                                    stdout=subprocess.PIPE,
-                                    stderr=subprocess.PIPE)
-            output, err = proc.communicate()
-        except Exception as e:
-            if err:
-                self.log.debug('Errors occoured while running following command to get N-V-R-E:')
-                self.log.debug(joined_cmd)
-                self.log.error(err)
-            raise rpkgError('Could not query n-v-r of %s: %s'
-                            % (self.repo_name, e))
-        if err:
-            self.log.debug('Errors occoured while running following command to get N-V-R-E:')
-            self.log.debug(joined_cmd)
-            self.log.error(err)
-        if proc.returncode > 0:
-            raise rpkgError('Could not get n-v-r-e from %s'
-                            % os.path.join(self.path, self.spec))
-
-        # Get just the output, then split it by ??, grab the first and split
-        # again to get ver and rel
-        first_line_output = output.split('??')[1]
-        parts = first_line_output.split()
-        if len(parts) != 4:
-            raise rpkgError('Could not get n-v-r-e from %r'
-                            % first_line_output)
-        (self._package_name_spec,
-         self._epoch,
-         self._ver,
-         self._rel) = parts
-
-        # Most packages don't include a "Epoch: 0" line, in which case RPM
-        # returns '(none)'
-        if self._epoch == "(none)":
-            self._epoch = "0"
-
-    @property
     def repo(self):
         """This property ensures the repo attribute"""
 
@@ -670,94 +380,12 @@ class Commands(object):
             raise rpkgError('%s is not a valid repo' % self.path)
 
     @property
-    def rpmdefines(self):
-        """This property ensures the rpm defines"""
-
-        if not self._rpmdefines:
-            self.load_rpmdefines()
-        return(self._rpmdefines)
-
-    def load_rpmdefines(self):
-        """Populate rpmdefines based on current active branch"""
-
-        # This is another function ripe for subclassing
-
-        try:
-            # This regex should find the 'rhel-5' or 'rhel-6.2' parts of the
-            # branch name.  There should only be one of those, and all branches
-            # should end in one.
-            osver = re.search(r'rhel-\d.*$', self.repo.branch_merge).group()
-        except AttributeError:
-            raise rpkgError('Could not find the base OS ver from branch name'
-                            ' %s. Consider using --release option' %
-                            self.repo.branch_merge)
-        self._distvar, self._distval = osver.split('-')
-        self._distval = self._distval.replace('.', '_')
-        self._disttag = 'el%s' % self._distval
-        self._rpmdefines = ["--define '_sourcedir %s'" % self.path,
-                            "--define '_specdir %s'" % self.path,
-                            "--define '_builddir %s'" % self.path,
-                            "--define '_srcrpmdir %s'" % self.path,
-                            "--define '_rpmdir %s'" % self.path,
-                            "--define 'dist .%s'" % self._disttag,
-                            "--define '%s %s'" % (self._distvar,
-                                                  self._distval.split('_')[0]),
-                            # int and float this to remove the decimal
-                            "--define '%s 1'" % self._disttag]
-
-    @property
-    def spec(self):
-        """This property ensures the spec attribute"""
-
-        if not self._spec:
-            self.load_spec()
-        return self._spec
-
-    def load_spec(self):
-        """This sets the spec attribute"""
-
-        deadpackage = False
-
-        # Get a list of files in the path we're looking at
-        files = os.listdir(self.path)
-        # Search the files for the first one that ends with ".spec"
-        for f in files:
-            if f.endswith('.spec') and not f.startswith('.'):
-                self._spec = f
-                return
-            if f == 'dead.package':
-                deadpackage = True
-        if deadpackage:
-            raise rpkgError('No spec file found. This package is retired')
-        else:
-            raise rpkgError('No spec file found.')
-
-    @property
-    def target(self):
-        """This property ensures the target attribute"""
-
-        if not self._target:
-            self.load_target()
-        return self._target
-
-    def load_target(self):
-        """This creates the target attribute based on branch merge"""
-
-        # If a site has a different naming scheme, this would be where
-        # a site would override
-        self._target = '%s-candidate' % self.repo.branch_merge
-
-    @property
-    def container_build_target(self):
-        """This property ensures the target for container builds."""
-        if not self._container_build_target:
-            self.load_container_build_target()
-        return self._container_build_target
-
-    def load_container_build_target(self):
-        """This creates a target based on git branch and namespace."""
-        self._container_build_target = '%s-%s-candidate' % (
-            self.repo.branch_merge, self.ns)
+    def package(self):
+        if not hasattr(self, '_package'):
+            self._package = PackageMetadata(
+                self.repo,
+                distgit_namespaced=self.distgit_namespaced)
+        return self._package
 
     @property
     def topurl(self):
@@ -822,20 +450,9 @@ class Commands(object):
         self._runas = runas
 
     @property
-    def ver(self):
-        """This property ensures the ver attribute"""
-        if not self._ver:
-            self.load_nameverrel()
-        return(self._ver)
-
-    @property
     def mock_results_dir(self):
-        return os.path.join(self.path, "results_%s" % self.repo_name,
-                            self.ver, self.rel)
-
-    @property
-    def sources_filename(self):
-        return os.path.join(self.path, 'sources')
+        return os.path.join(self.path, "results_%s" % self.package.repo_name,
+                            self.package.ver, self.package.rel)
 
     @property
     def osbs_config_filename(self):
@@ -947,7 +564,7 @@ class Commands(object):
     def _get_build_arches_from_spec(self):
         """Given the path to an spec, retrieve the build arches"""
 
-        spec = os.path.join(self.path, self.spec)
+        spec = os.path.join(self.path, self.package.spec)
         try:
             hdr = rpm.spec(spec)
         except Exception:
@@ -1573,7 +1190,7 @@ class Commands(object):
         """
 
         # Create the outfile name based on arguments
-        outfile = '%s-%s-%s.patch' % (self.repo_name, self.ver, suffix)
+        outfile = '%s-%s-%s.patch' % (self.package.repo_name, self.package.ver, suffix)
 
         # If we want to rediff, the patch file has to already exist
         if rediff and not os.path.exists(os.path.join(self.path, outfile)):
@@ -1581,13 +1198,13 @@ class Commands(object):
                             os.path.join(self.path, outfile))
 
         # See if there is a source dir to diff in
-        if not os.path.isdir(os.path.join(self.path,
-                                          '%s-%s' % (self.repo_name,
-                                                     self.ver))):
+        expected_source_dir = os.path.join(
+            self.path, '%s-%s' % (self.package.repo_name, self.package.ver))
+        if not os.path.isdir(expected_source_dir):
             raise rpkgError('Expanded source dir not found!')
 
         # Setup the command
-        cmd = ['gendiff', '%s-%s' % (self.repo_name, self.ver),
+        cmd = ['gendiff', '%s-%s' % (self.package.repo_name, self.package.ver),
                '.%s' % suffix]
 
         # Try to run the command and capture the output
@@ -1658,7 +1275,7 @@ class Commands(object):
                            in glob.glob(file_pattern)]
 
         git_tree = self.repo.repo.head.commit.tree
-        sources_file = SourcesFile(self.sources_filename,
+        sources_file = SourcesFile(self.package.sources_filename,
                                    self.source_entry_type)
 
         patches_not_tracked = [
@@ -1694,7 +1311,7 @@ class Commands(object):
     def sources(self, outdir=None):
         """Download source files"""
 
-        if not os.path.exists(self.sources_filename):
+        if not os.path.exists(self.package.sources_filename):
             self.log.info("sources file doesn't exist. Source files download skipped.")
             return
 
@@ -1702,7 +1319,7 @@ class Commands(object):
         if not outdir:
             outdir = self.path
 
-        sourcesf = SourcesFile(self.sources_filename, self.source_entry_type)
+        sourcesf = SourcesFile(self.package.sources_filename, self.source_entry_type)
 
         args = dict()
         if self.lookaside_request_params:
@@ -1716,11 +1333,39 @@ class Commands(object):
 
         for entry in sourcesf.entries:
             outfile = os.path.join(outdir, entry.file)
+            if self.lookaside_namespaced:
+                repo_name = self.package.ns_repo_name
+            else:
+                repo_name = self.package.repo_name
             self.lookasidecache.download(
-                self.ns_repo_name if self.lookaside_namespaced else self.repo_name,
-                entry.file, entry.hash, outfile,
+                repo_name, entry.file, entry.hash, outfile,
                 hashtype=entry.hashtype, **args)
 
+    def check_repo(self, has_namespace=True, **criteria):
+        self.repo.check(**criteria)
+
+        if has_namespace:
+            try:
+                repo_name = self.repo.push_url
+            except rpkgError:
+                # Ignore error if cannot get remote push URL from this repo.
+                # That is we just skip has_namespace check when that error
+                # happens.
+                pass
+            else:
+                parts = urllib.parse.urlparse(repo_name)
+                parts = [p for p in parts.path.split('/') if p]
+                not_contain_namespace = len(parts) == 1
+                if not_contain_namespace:
+                    self.log.warning(
+                        'Your git configuration does not use a namespace.')
+                    self.log.warning(
+                        'Consider updating your git configuration by running:')
+                    self.log.warning(
+                        '  git remote set-url %s %s',
+                        self.repo.branch_remote,
+                        self._get_namespace_giturl(parts[0]))
+
     def switch_branch(self, branch, fetch=True):
         """Switch the working branch
 
@@ -1734,7 +1379,7 @@ class Commands(object):
         # the first remote it finds.  When multiple remotes are in play
         # this needs to get smarter
 
-        self.repo.check(all_pushed=False)
+        self.check_repo(all_pushed=False)
 
         # Get our list of branches
         self.log.debug('Listing refs')
@@ -1793,7 +1438,8 @@ class Commands(object):
         :rtype: str
         """
         return '{0}#{1}'.format(
-            self._get_namespace_anongiturl(repo_name or self.ns_repo_name),
+            self._get_namespace_anongiturl(
+                repo_name or self.package.ns_repo_name),
             commit_hash or self.repo.commit_hash)
 
     def build(self, skip_tag=False, scratch=False, background=False,
@@ -1835,7 +1481,7 @@ class Commands(object):
             # Check to see if the tree is dirty and if all local commits
             # are pushed
             try:
-                self.repo.check()
+                self.check_repo()
             except rpkgError as e:
                 msg = '{0}\n{1}'.format(
                     str(e),
@@ -1843,9 +1489,9 @@ class Commands(object):
                 raise rpkgError(msg)
             url = self.construct_build_url()
         # Check to see if the target is valid
-        build_target = self.kojisession.getBuildTarget(self.target)
+        build_target = self.kojisession.getBuildTarget(self.package.target)
         if not build_target:
-            raise rpkgError('Unknown build target: %s' % self.target)
+            raise rpkgError('Unknown build target: %s' % self.package.target)
         # see if the dest tag is locked
         dest_tag = self.kojisession.getTag(build_target['dest_tag_name'])
         if not dest_tag:
@@ -1885,14 +1531,14 @@ class Commands(object):
             cmd.append('--arch-override=%s' % ','.join(arches))
             opts['arch_override'] = ' '.join(arches)
 
-        cmd.append(self.target)
+        cmd.append(self.package.target)
 
         if url.endswith('.src.rpm'):
             srpm = os.path.basename(url)
             build_reference = srpm
         else:
             try:
-                build_reference = self.nvr
+                build_reference = self.package.nvr
             except rpkgError as error:
                 self.log.warning(error)
                 if nvr_check:
@@ -1905,18 +1551,18 @@ class Commands(object):
                                   ' care that I am not able to construct NVR.'
                                   '  I will refer this build by package name'
                                   ' in following messages.')
-                    build_reference = self.repo_name
+                    build_reference = self.package.repo_name
 
         # see if this build has been done.  Does not check builds within
         # a chain
         if nvr_check and not scratch and not url.endswith('.src.rpm'):
-            build = self.kojisession.getBuild(self.nvr)
+            build = self.kojisession.getBuild(self.package.nvr)
             if build:
                 if build['state'] == 1:
                     raise rpkgError('Package %s has already been built\n'
                                     'Note: You can skip this check with'
                                     ' --skip-nvr-check. See help for more'
-                                    ' info.' % self.nvr)
+                                    ' info.' % self.package.nvr)
         # Now submit the task and get the task_id to return
         # Handle the chain build version
         if chain:
@@ -1932,19 +1578,21 @@ class Commands(object):
             # This next list comp is ugly, but it's how we properly get a :
             # put in between each build set
             cmd.extend(' : '.join([' '.join(build_sets) for build_sets in chain]).split())
-            self.log.info('Chain building %s + %s for %s', build_reference, chain[:-1], self.target)
+            self.log.info('Chain building %s + %s for %s',
+                          build_reference, chain[:-1], self.package.target)
             self.log.debug('Building chain %s for %s with options %s and a priority of %s',
-                           chain, self.target, opts, priority)
+                           chain, self.package.target, opts, priority)
             self.log.debug(' '.join(cmd))
-            task_id = self.kojisession.chainBuild(chain, self.target, opts, priority=priority)
+            task_id = self.kojisession.chainBuild(
+                chain, self.package.target, opts, priority=priority)
         # Now handle the normal build
         else:
             cmd.append(url)
-            self.log.info('Building %s for %s', build_reference, self.target)
+            self.log.info('Building %s for %s', build_reference, self.package.target)
             self.log.debug('Building %s for %s with options %s and a priority of %s',
-                           url, self.target, opts, priority)
+                           url, self.package.target, opts, priority)
             self.log.debug(' '.join(cmd))
-            task_id = self.kojisession.build(url, self.target, opts, priority=priority)
+            task_id = self.kojisession.build(url, self.package.target, opts, priority=priority)
         self.log.info('Created task: %s', task_id)
         self.log.info('Task info: %s/taskinfo?taskID=%s', self.kojiweburl, task_id)
         return task_id
@@ -1952,9 +1600,13 @@ class Commands(object):
     def clog(self, raw=False):
         """Write the latest spec changelog entry to a clog file"""
 
-        spec_file = os.path.join(self.path, self.spec)
-        cmd = ['rpm'] + self.rpmdefines + ['-q', '--qf', '"%{CHANGELOGTEXT}\n"',
-                                           '--specfile', '"%s"' % spec_file]
+        spec_file = os.path.join(self.path, self.package.spec)
+        cmd = (
+            ['rpm'] +
+            self.package.rpmdefines +
+            ['-q', '--qf', '"%{CHANGELOGTEXT}\n"', '--specfile',
+             '"%s"' % spec_file]
+        )
         proc = subprocess.Popen(' '.join(cmd), shell=True,
                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                                 universal_newlines=True)
@@ -1994,7 +1646,7 @@ class Commands(object):
 
         # setup the rpm command
         cmd = ['rpmbuild']
-        cmd.extend(self.rpmdefines)
+        cmd.extend(self.package.rpmdefines)
         if builddir:
             # Tack on a new builddir to the end of the defines
             cmd.append("--define '_builddir %s'" % os.path.abspath(builddir))
@@ -2006,13 +1658,13 @@ class Commands(object):
             cmd.append('--nocheck')
         if self.quiet:
             cmd.append('--quiet')
-        cmd.extend(['-bc', os.path.join(self.path, self.spec)])
+        cmd.extend(['-bc', os.path.join(self.path, self.package.spec)])
         # Run the command
         self._run_command(cmd, shell=True)
 
     def giturl(self):
         """Return the git url that would be used for building"""
-        self.repo.check(is_dirty=False, all_pushed=False)
+        self.check_repo(is_dirty=False, all_pushed=False)
         return self.construct_build_url()
 
     def koji_upload(self, file, path, callback=None):
@@ -2047,7 +1699,7 @@ class Commands(object):
 
         # setup the rpm command
         cmd = ['rpmbuild']
-        cmd.extend(self.rpmdefines)
+        cmd.extend(self.package.rpmdefines)
         if builddir:
             # Tack on a new builddir to the end of the defines
             cmd.append("--define '_builddir %s'" % os.path.abspath(builddir))
@@ -2059,7 +1711,7 @@ class Commands(object):
             cmd.append('--nocheck')
         if self.quiet:
             cmd.append('--quiet')
-        cmd.extend(['-bi', os.path.join(self.path, self.spec)])
+        cmd.extend(['-bi', os.path.join(self.path, self.package.spec)])
         # Run the command
         self._run_command(cmd, shell=True)
         return
@@ -2073,7 +1725,8 @@ class Commands(object):
         """
 
         # Check for srpm
-        srpm = "%s-%s-%s.src.rpm" % (self.repo_name, self.ver, self.rel)
+        srpm = "%s-%s-%s.src.rpm" % (
+            self.package.repo_name, self.package.ver, self.package.rel)
         if not os.path.exists(os.path.join(self.path, srpm)):
             log.warning('No srpm found')
 
@@ -2088,7 +1741,7 @@ class Commands(object):
         if not rpms:
             log.warning('No rpm found')
         cmd = ['rpmlint']
-        default_rpmlintconf = '{0}.rpmlintrc'.format(self.repo_name)
+        default_rpmlintconf = '{0}.rpmlintrc'.format(self.package.repo_name)
         if info:
             cmd.extend(['-i'])
         if rpmlintconf:
@@ -2100,7 +1753,7 @@ class Commands(object):
             self.log.warning('.rpmlint file usage as default rpmlint configuration is deprecated '
                              'and will be removed in future version. '
                              'Use {0} instead'.format(default_rpmlintconf))
-        cmd.append(os.path.join(self.path, self.spec))
+        cmd.append(os.path.join(self.path, self.package.spec))
         if os.path.exists(os.path.join(self.path, srpm)):
             cmd.append(os.path.join(self.path, srpm))
         cmd.extend(sorted(rpms))
@@ -2128,7 +1781,7 @@ class Commands(object):
         # Get the sources
         # build up the rpm command
         cmd = ['rpmbuild']
-        cmd.extend(self.rpmdefines + localargs)
+        cmd.extend(self.package.rpmdefines + localargs)
         if builddir:
             # Tack on a new builddir to the end of the defines
             cmd.append("--define '_builddir %s'" % os.path.abspath(builddir))
@@ -2146,8 +1799,8 @@ class Commands(object):
             cmd.extend(['--target', arch])
         if self.quiet:
             cmd.append('--quiet')
-        cmd.extend(['-ba', os.path.join(self.path, self.spec)])
-        logfile = '.build-%s-%s.log' % (self.ver, self.rel)
+        cmd.extend(['-ba', os.path.join(self.path, self.package.spec)])
+        logfile = '.build-%s-%s.log' % (self.package.ver, self.package.rel)
 
         cmd = '%s | tee %s' % (' '.join(cmd), logfile)
         try:
@@ -2174,9 +1827,9 @@ class Commands(object):
 
         # Figure out some things about ourself.
         if not target:
-            target = self.target
+            target = self.package.target
         if not arch:
-            arch = self.localarch
+            arch = self.package.localarch
 
         # Figure out if we have a valid build target
         build_target = self.anon_kojisession.getBuildTarget(target)
@@ -2192,7 +1845,7 @@ class Commands(object):
 
         # Generate the config
         config = koji.genMockConfig('%s-%s' % (target, arch), arch,
-                                    distribution=self.disttag,
+                                    distribution=self.package.disttag,
                                     tag_name=build_target['build_tag_name'],
                                     repoid=repoid,
                                     topurl=self.topurl)
@@ -2236,7 +1889,7 @@ class Commands(object):
         Returns used config directory
         """
         if not root:
-            root = self.mockconfig
+            root = self.package.mockconfig
         if not config_dir:
             my_config_dir = tempfile.mkdtemp(prefix="%s." % root,
                                              suffix='mockconfig')
@@ -2295,7 +1948,7 @@ class Commands(object):
 
         config_dir = None
         if not root:
-            root = self.mockconfig
+            root = self.package.mockconfig
             chroot_cfg = '/etc/mock/%s.cfg' % root
             if not os.path.exists(chroot_cfg):
                 self.log.debug('Mock config %s was not found. Going to'
@@ -2335,7 +1988,8 @@ class Commands(object):
         :raises rpkgError: if failed to add a file to file `sources`.
         """
 
-        sourcesf = SourcesFile(self.sources_filename, self.source_entry_type,
+        sourcesf = SourcesFile(self.package.sources_filename,
+                               self.source_entry_type,
                                replace=replace)
         gitignore = GitIgnore(os.path.join(self.path, '.gitignore'))
 
@@ -2356,15 +2010,19 @@ class Commands(object):
                     '    `%(arg0)s new-sources file1 file2 ...`']) % {
                         'newhash': e.new_hashtype,
                         'existinghash': e.existing_hashtype,
-                        'sources': self.sources_filename,
+                        'sources': self.package.sources_filename,
                         'arg0': sys.argv[0],
                     }
                 raise rpkgError(msg)
 
             gitignore.add('/%s' % file_basename)
-            self.lookasidecache.upload(
-                self.ns_repo_name if self.lookaside_namespaced else self.repo_name,
-                f, file_hash)
+
+            if self.lookaside_namespaced:
+                repo_name = self.package.ns_repo_name
+            else:
+                repo_name = self.package.repo_name
+
+            self.lookasidecache.upload(repo_name, f, file_hash)
 
         sourcesf.write()
         gitignore.write()
@@ -2381,7 +2039,7 @@ class Commands(object):
 
         # setup the rpm command
         cmd = ['rpmbuild']
-        cmd.extend(self.rpmdefines)
+        cmd.extend(self.package.rpmdefines)
         if builddir:
             # Tack on a new builddir to the end of the defines
             cmd.append("--define '_builddir %s'" % os.path.abspath(builddir))
@@ -2389,7 +2047,7 @@ class Commands(object):
             cmd.extend(['--target', arch])
         if self.quiet:
             cmd.append('--quiet')
-        cmd.extend(['--nodeps', '-bp', os.path.join(self.path, self.spec)])
+        cmd.extend(['--nodeps', '-bp', os.path.join(self.path, self.package.spec)])
         # Run the command
         self._run_command(cmd, shell=True)
 
@@ -2403,16 +2061,14 @@ class Commands(object):
             digests.
         """
 
-        self.srpmname = os.path.join(self.path,
-                                     "%s-%s-%s.src.rpm"
-                                     % (self.repo_name, self.ver, self.rel))
+        self.srpmname = os.path.join(self.path, self.package.srpm_filename)
 
         # See if we need to build the srpm
         if os.path.exists(self.srpmname):
             self.log.debug('Srpm found, rewriting it.')
 
         cmd = ['rpmbuild']
-        cmd.extend(self.rpmdefines)
+        cmd.extend(self.package.rpmdefines)
         if self.quiet:
             cmd.append('--quiet')
         # Figure out which hashtype to use, if not provided one
@@ -2425,7 +2081,7 @@ class Commands(object):
                         % hashtype,
                         "--define '_binary_filedigest_algorithm %s'"
                         % hashtype])
-        cmd.extend(['--nodeps', '-bs', os.path.join(self.path, self.spec)])
+        cmd.extend(['--nodeps', '-bs', os.path.join(self.path, self.package.spec)])
         self._run_command(cmd, shell=True)
 
     def unused_patches(self):
@@ -2438,7 +2094,7 @@ class Commands(object):
         # Create a list for unused patches
         unused = []
         # Get the content of spec into memory for fast searching
-        with open(os.path.join(self.path, self.spec), 'r') as f:
+        with open(os.path.join(self.path, self.package.spec), 'r') as f:
             data = f.read()
         if six.PY2:
             try:
@@ -2452,9 +2108,9 @@ class Commands(object):
         else:
             spec = data
         # Replace %{name} with the package name
-        spec = spec.replace("%{name}", self.repo_name)
+        spec = spec.replace("%{name}", self.package.repo_name)
         # Replace %{version} with the package version
-        spec = spec.replace("%{version}", self.ver)
+        spec = spec.replace("%{version}", self.package.ver)
 
         # Get a list of files tracked in source control
         files = self.repo.git.ls_files('--exclude-standard').split()
@@ -2492,13 +2148,13 @@ class Commands(object):
 
         # setup the rpm command
         cmd = ['rpmbuild']
-        cmd.extend(self.rpmdefines)
+        cmd.extend(self.package.rpmdefines)
         if builddir:
             # Tack on a new builddir to the end of the defines
             cmd.append("--define '_builddir %s'" % os.path.abspath(builddir))
         if self.quiet:
             cmd.append('--quiet')
-        cmd.extend(['-bl', os.path.join(self.path, self.spec)])
+        cmd.extend(['-bl', os.path.join(self.path, self.package.spec)])
         # Run the command
         self._run_command(cmd, shell=True)
 
@@ -2508,8 +2164,11 @@ class Commands(object):
                              koji_task_watcher=None,
                              nowait=False):
         # check if repo is dirty and all commits are pushed
-        self.repo.check()
-        container_target = self.target if target_override else self.container_build_target
+        self.check_repo()
+        if target_override:
+            container_target = self.package.target
+        else:
+            container_target = self.package.container_build_target
 
         # This is for backward-compatibility of deprecated kojiconfig.
         # Signature of container_build_koji is not changed in case someone
@@ -2755,7 +2414,7 @@ class Commands(object):
         if not scm_url and check_repo:
             # Make sure the local repo is clean (no unpushed changes) if the
             # user didn't specify an SCM URL
-            self.repo.check()
+            self.check_repo()
 
         if branch:
             actual_branch = branch
@@ -2775,7 +2434,7 @@ class Commands(object):
             # If the scm_url isn't specified, get the remote git URL of the
             # git repo the current user is in
             actual_scm_url = self._get_namespace_anongiturl(
-                self.ns_repo_name)
+                self.package.ns_repo_name)
             actual_scm_url = '{0}?#{1}'.format(actual_scm_url, self.repo.commit_hash)
         return actual_scm_url, actual_branch
 
diff --git a/pyrpkg/cli.py b/pyrpkg/cli.py
index adda37a..bb6d4eb 100644
--- a/pyrpkg/cli.py
+++ b/pyrpkg/cli.py
@@ -277,21 +277,23 @@ class cliClient(object):
                                        )
 
         if self.args.module_name:
+            this_pkg = self._cmd.package
             # Repository name was specified via argument
             if '/' not in self.args.module_name:
-                self._cmd.repo_name = self.args.module_name
+                this_pkg.repo_name = self.args.module_name
                 if dg_namespaced:
                     # No slash, assume rpms namespace
-                    self._cmd.ns = 'rpms'
+                    this_pkg.ns = 'rpms'
             else:
-                self._cmd.ns, self._cmd.repo_name = self.args.module_name.rsplit('/', 1)
+                this_pkg.ns, this_pkg.module_name = self.args.module_name.rsplit('/', 1)
 
         if self.args.repo_name:
-            self._cmd.repo_name = self.args.repo_name
+            this_pkg = self._cmd.package
+            this_pkg.repo_name = self.args.repo_name
             if dg_namespaced and self.args.repo_namespace:
-                self._cmd.ns = self.args.repo_namespace
+                this_pkg.ns = self.args.repo_namespace
             else:
-                self._cmd.ns = 'rpms'
+                this_pkg.ns = 'rpms'
 
         self._cmd.password = self.args.password
         self._cmd.runas = self.args.runas
@@ -1439,7 +1441,7 @@ see API KEY section of copr-cli(1) man page.
             if self.args.srpm == 'CONSTRUCT':
                 self.log.debug('Generating an srpm')
                 self.srpm()
-                self.args.srpm = '%s.src.rpm' % self.cmd.nvr
+                self.args.srpm = '%s.src.rpm' % self.cmd.package.nvr
             # Figure out if we want a verbose output or not
             callback = None
             if not self.args.q:
@@ -1480,8 +1482,8 @@ see API KEY section of copr-cli(1) man page.
         return koji_cli.lib.watch_tasks(self.cmd.kojisession, [task_id])
 
     def chainbuild(self):
-        if self.cmd.module_name in self.args.package:
-            raise Exception('%s must not be in the chain' % self.cmd.module_name)
+        if self.cmd.package.module_name in self.args.package:
+            raise Exception('%s must not be in the chain' % self.cmd.package.module_name)
 
         # make sure we didn't get an empty chain
         if self.args.package == [':']:
@@ -1581,7 +1583,7 @@ see API KEY section of copr-cli(1) man page.
         try:
             self.cmd.commit(self.args.message, self.args.file, self.args.files, self.args.signoff)
             if self.args.tag:
-                tagname = self.cmd.nvr
+                tagname = self.cmd.package.nvr
                 self.cmd.add_tag(tagname, True, self.args.message, self.args.file)
         except Exception:
             if self.args.tag:
@@ -1677,7 +1679,7 @@ see API KEY section of copr-cli(1) man page.
         self.log.debug('Generating an srpm')
         self.args.hash = None
         self.srpm()
-        srpm_name = '%s.src.rpm' % self.cmd.nvr
+        srpm_name = '%s.src.rpm' % self.cmd.package.nvr
         self.cmd.copr_build(self.args.project[0],
                             srpm_name,
                             self.args.nowait,
@@ -1687,7 +1689,7 @@ see API KEY section of copr-cli(1) man page.
         self.cmd.diff(self.args.cached, self.args.files)
 
     def gimmespec(self):
-        print(self.cmd.spec)
+        print(self.cmd.package.spec)
 
     def gitbuildhash(self):
         print(self.cmd.gitbuildhash(self.args.build))
@@ -1822,7 +1824,7 @@ see API KEY section of copr-cli(1) man page.
             stream = self.args.stream
 
         if not self.args.file_path:
-            file_path = os.path.join(self.cmd.path, self.cmd.module_name + ".yaml")
+            file_path = os.path.join(self.cmd.path, self.cmd.package.module_name + ".yaml")
         else:
             file_path = self.args.file_path
 
@@ -2074,7 +2076,7 @@ see API KEY section of copr-cli(1) man page.
             tagname = self.args.tag
             if not tagname or self.args.clog:
                 if not tagname:
-                    tagname = self.cmd.nvr
+                    tagname = self.cmd.package.nvr
                 if self.args.clog:
                     self.cmd.clog(self.args.raw)
                     filename = 'clog'
@@ -2089,8 +2091,8 @@ see API KEY section of copr-cli(1) man page.
         self.cmd.verify_files(builddir=self.args.builddir)
 
     def verrel(self):
-        print('%s-%s-%s' % (self.cmd.module_name, self.cmd.ver,
-                            self.cmd.rel))
+        this_pkg = self.cmd.package
+        print('%s-%s-%s' % (this_pkg.module_name, this_pkg.ver, this_pkg.rel))
 
     # Stole these three functions from /usr/bin/koji
     def setupLogging(self, log):
diff --git a/pyrpkg/pkginfo.py b/pyrpkg/pkginfo.py
new file mode 100644
index 0000000..c9bdcc5
--- /dev/null
+++ b/pyrpkg/pkginfo.py
@@ -0,0 +1,394 @@
+# pyrpkg - a Python library for RPM Packagers
+#
+# Copyright (C) 2018 Red Hat Inc.
+# Author(s): Chenxiong Qi <cqi@redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
+# the full text of the license.
+
+import logging
+import os
+import re
+import subprocess
+import posixpath
+
+from six.moves import urllib
+from pyrpkg.errors import rpkgError
+
+logger = logging.getLogger(__name__)
+
+
+class PackageMetadata(object):
+    """Package metadata"""
+
+    def __init__(self, repo, overwrite_target=None, distgit_namespaced=True):
+        """Initialization
+
+        :param repo: object of ``PackageRepo`` representing underlying package
+            repository.
+        :type repo: :class:`PackageRepository <pyrpkg.pkgrepo.PackageRepository>`
+        :param bool distgit_namespaced: whether namespace is enabled in
+            dist-git.
+        """
+        self.repo = repo
+        self.distgit_namespaced = distgit_namespaced
+
+        self._disttag = None
+        self._distval = None
+        self._distvar = None
+        self._epoch = None
+        self._localarch = None
+        self._mockconfig = None
+        self._module_name = None
+        self._nvr = None
+        self._spec = None
+        self._rpmdefines = None
+        self._ver = None
+        self._ns = None
+
+        # The build target for containers within the buildsystem
+        self._container_build_target = overwrite_target
+        self._target = overwrite_target
+
+        # Deprecates self.module_name
+        self._repo_name = None
+
+    @property
+    def disttag(self):
+        """This property ensures the disttag attribute"""
+
+        if not self._disttag:
+            self.load_rpmdefines()
+        return self._disttag
+
+    @property
+    def distval(self):
+        """This property ensures the distval attribute"""
+
+        if not self._distval:
+            self.load_rpmdefines()
+        return self._distval
+
+    @property
+    def distvar(self):
+        """This property ensures the distvar attribute"""
+
+        if not self._distvar:
+            self.load_rpmdefines()
+        return self._distvar
+
+    @property
+    def epoch(self):
+        """This property ensures the epoch attribute"""
+
+        if not self._epoch:
+            self.load_nameverrel()
+        return self._epoch
+
+    @property
+    def localarch(self):
+        """This property ensures the module attribute"""
+
+        if not self._localarch:
+            self.load_localarch()
+        return(self._localarch)
+
+    def load_localarch(self):
+        """Get the local arch as defined by rpm"""
+
+        proc = subprocess.Popen(['rpm --eval %{_arch}'], shell=True,
+                                stdout=subprocess.PIPE,
+                                universal_newlines=True)
+        self._localarch = proc.communicate()[0].strip('\n')
+
+    @property
+    def mockconfig(self):
+        """This property ensures the mockconfig attribute"""
+
+        if not self._mockconfig:
+            self.load_mockconfig()
+        return self._mockconfig
+
+    @mockconfig.setter
+    def mockconfig(self, config):
+        self._mockconfig = config
+
+    def load_mockconfig(self):
+        """This sets the mockconfig attribute"""
+
+        self._mockconfig = '%s-%s' % (self.target, self.localarch)
+
+    @property
+    def ns(self):
+        """This property provides the namespace of the module"""
+
+        if not self._ns:
+            self.load_ns()
+        return self._ns
+
+    @ns.setter
+    def ns(self, ns):
+        self._ns = ns
+
+    def load_ns(self):
+        """Loads the namespace"""
+
+        try:
+            if self.distgit_namespaced:
+                if self.repo.push_url:
+                    parts = urllib.parse.urlparse(self.repo.push_url)
+
+                    path_parts = [p for p in parts.path.split("/") if p]
+                    if len(path_parts) == 1:
+                        path_parts.insert(0, "rpms")
+                    ns = path_parts[-2]
+
+                    self._ns = ns
+            else:
+                self._ns = None
+                logger.info("Could not find ns, distgit is not namespaced")
+        except rpkgError:
+            logger.warning('Failed to get ns from Git url or pushurl')
+
+    @property
+    def ns_module_name(self):
+        logger.warning('Property ns_module_name is deprecated. Please use '
+                         'ns_repo_name instead.')
+        return self.ns_repo_name
+
+    @property
+    def ns_repo_name(self):
+        if self.distgit_namespaced:
+            return '%s/%s' % (self.ns, self.repo_name)
+        else:
+            return self.repo_name
+
+    @property
+    def nvr(self):
+        """This property ensures the nvr attribute"""
+
+        if not self._nvr:
+            self.load_nvr()
+        return self._nvr
+
+    def load_nvr(self):
+        """This sets the nvr attribute"""
+        self._nvr = '{0}-{1}-{2}'.format(self.repo_name, self.ver, self.rel)
+
+    @property
+    def rel(self):
+        """This property ensures the rel attribute"""
+        if not self._rel:
+            self.load_nameverrel()
+        return(self._rel)
+
+    def load_nameverrel(self):
+        """Set the release of a package module."""
+
+        cmd = ['rpm']
+        cmd.extend(self.rpmdefines)
+        # We make sure there is a space at the end of our query so that
+        # we can split it later.  When there are subpackages, we get a
+        # listing for each subpackage.  We only care about the first.
+        cmd.extend(['-q', '--qf', '"%{NAME} %{EPOCH} %{VERSION} %{RELEASE}??"',
+                    '--specfile', '"%s"' % os.path.join(self.repo.path, self.spec)])
+        joined_cmd = ' '.join(cmd)
+        try:
+            proc = subprocess.Popen(joined_cmd, shell=True,
+                                    universal_newlines=True,
+                                    stdout=subprocess.PIPE,
+                                    stderr=subprocess.PIPE)
+            output, err = proc.communicate()
+        except Exception as e:
+            if err:
+                logger.debug('Errors occoured while running following command to get N-V-R-E:')
+                logger.debug(joined_cmd)
+                logger.error(err)
+            raise rpkgError('Could not query n-v-r of %s: %s'
+                            % (self.module_name, e))
+        if err:
+            logger.debug('Errors occoured while running following command to get N-V-R-E:')
+            logger.debug(joined_cmd)
+            logger.error(err)
+        # Get just the output, then split it by ??, grab the first and split
+        # again to get ver and rel
+        first_line_output = output.split('??')[0]
+        parts = first_line_output.split()
+        if len(parts) != 4:
+            raise rpkgError('Could not get n-v-r-e from %r'
+                            % first_line_output)
+        (self._package_name_spec,
+         self._epoch,
+         self._ver,
+         self._rel) = parts
+
+        # Most packages don't include a "Epoch: 0" line, in which case RPM
+        # returns '(none)'
+        if self._epoch == "(none)":
+            self._epoch = "0"
+
+    @property
+    def rpmdefines(self):
+        """This property ensures the rpm defines"""
+
+        if not self._rpmdefines:
+            self.load_rpmdefines()
+        return(self._rpmdefines)
+
+    def load_rpmdefines(self):
+        """Populate rpmdefines based on current active branch"""
+
+        # This is another function ripe for subclassing
+
+        try:
+            # This regex should find the 'rhel-5' or 'rhel-6.2' parts of the
+            # branch name.  There should only be one of those, and all branches
+            # should end in one.
+            osver = re.search(r'rhel-\d.*$', self.repo.branch_merge).group()
+        except AttributeError:
+            raise rpkgError('Could not find the base OS ver from branch name'
+                            ' %s. Consider using --release option' %
+                            self.repo.branch_merge)
+        self._distvar, self._distval = osver.split('-')
+        self._distval = self._distval.replace('.', '_')
+        self._disttag = 'el%s' % self._distval
+        self._rpmdefines = [
+            "--define '_sourcedir %s'" % self.repo.path,
+            "--define '_specdir %s'" % self.repo.path,
+            "--define '_builddir %s'" % self.repo.path,
+            "--define '_srcrpmdir %s'" % self.repo.path,
+            "--define '_rpmdir %s'" % self.repo.path,
+            "--define 'dist .%s'" % self._disttag,
+            "--define '%s %s'" % (self._distvar, self._distval.split('_')[0]),
+            # int and float this to remove the decimal
+            "--define '%s 1'" % self._disttag
+        ]
+
+    @property
+    def spec(self):
+        """This property ensures the module attribute"""
+
+        if not self._spec:
+            self.load_spec()
+        return self._spec
+
+    def load_spec(self):
+        """This sets the spec attribute"""
+
+        deadpackage = False
+
+        # Get a list of files in the path we're looking at
+        files = os.listdir(self.repo.path)
+        # Search the files for the first one that ends with ".spec"
+        for f in files:
+            if f.endswith('.spec') and not f.startswith('.'):
+                self._spec = f
+                return
+            if f == 'dead.package':
+                deadpackage = True
+        if deadpackage:
+            raise rpkgError('No spec file found. This package is retired')
+        else:
+            raise rpkgError('No spec file found.')
+
+    @property
+    def target(self):
+        """This property ensures the target attribute"""
+
+        if not self._target:
+            self.load_target()
+        return self._target
+
+    def load_target(self):
+        """This creates the target attribute based on branch merge"""
+
+        # If a site has a different naming scheme, this would be where
+        # a site would override
+        self._target = '{0}-candidate'.format(self.repo.branch_merge)
+
+    @property
+    def container_build_target(self):
+        """This property ensures the target for container builds."""
+        if not self._container_build_target:
+            self.load_container_build_target()
+        return self._container_build_target
+
+    def load_container_build_target(self):
+        """This creates a target based on git branch and namespace."""
+        self._container_build_target = '{0}-{1}-candidate'.format(
+            self.repo.branch_merge, self.ns)
+
+    @property
+    def ver(self):
+        """This property ensures the ver attribute"""
+        if not self._ver:
+            self.load_nameverrel()
+        return(self._ver)
+
+    @property
+    def sources_filename(self):
+        """File name of sources"""
+        return os.path.join(self.repo.path, 'sources')
+
+    @property
+    def srpm_filename(self):
+        return '{0}-{1}-{2}.src.rpm'.format(self.module_name, self.ver, self.rel)
+
+    @property
+    def repo_name(self):
+        """Property to get repository name"""
+        if not self._repo_name:
+            self.load_repo_name()
+        return self._repo_name
+
+    @repo_name.setter
+    def repo_name(self, name):
+        """Set repository name"""
+        self._repo_name = name
+
+    @property
+    def module_name(self):
+        """This property ensures the module attribute"""
+        logger.warning('Property module_name is deprecated. Please use '
+                         'repo_name instead.')
+        return self.repo_name
+
+    @module_name.setter
+    def module_name(self, module_name):
+        logger.warning('Property module_name is deprecated. Please use '
+                         'repo_name instead.')
+        self.repo_name = module_name
+
+    def load_module_name(self):
+        return self.load_repo_name()
+
+    def load_repo_name(self):
+        """Loads repository name"""
+
+        try:
+            if self.repo.push_url:
+                parts = urllib.parse.urlparse(self.repo.push_url)
+
+                # FIXME
+                # if self.distgit_namespaced:
+                #     self._module_name = "/".join(parts.path.split("/")[-2:])
+                repo_name = posixpath.basename(parts.path.strip('/'))
+
+                if repo_name.endswith('.git'):
+                    repo_name = repo_name[:-len('.git')]
+                self._repo_name = repo_name
+                return
+        except rpkgError:
+            logger.warning('Failed to get repository name from Git url or pushurl')
+
+        self.load_nameverrel()
+        if self._package_name_spec:
+            self._repo_name = self._package_name_spec
+            return
+
+        raise rpkgError('Could not find current repository name.'
+                        ' Use --name and optional --namespace.')
+
diff --git a/tests/commands/test_package_name.py b/tests/commands/test_package_name.py
index 6c61856..e6b26ac 100644
--- a/tests/commands/test_package_name.py
+++ b/tests/commands/test_package_name.py
@@ -20,5 +20,5 @@ class CommandPackageNameTestCase(CommandTestCase):
         moduledir = os.path.join(self.path, self.module)
         cmd.path = moduledir
 
-        self.assertNotEqual(type(cmd.module_name), six.binary_type)
-        self.assertEqual(type(cmd.module_name), six.text_type)
+        self.assertNotEqual(type(cmd.package.repo_name), six.binary_type)
+        self.assertEqual(type(cmd.package.repo_name), six.text_type)
diff --git a/tests/commands/test_push.py b/tests/commands/test_push.py
index ee70664..b54e428 100644
--- a/tests/commands/test_push.py
+++ b/tests/commands/test_push.py
@@ -104,7 +104,7 @@ Patch3: d.path
 
         # Track c.patch in sources
         from pyrpkg.sources import SourcesFile
-        sources_file = SourcesFile(self.cmd.sources_filename,
+        sources_file = SourcesFile(self.cmd.package.sources_filename,
                                    self.cmd.source_entry_type)
         file_hash = self.cmd.lookasidecache.hash_file('c.patch')
         sources_file.add_entry(self.cmd.lookasidehash, 'c.patch', file_hash)
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 03eac68..fd58666 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -105,28 +105,28 @@ class TestModuleNameOption(CliTestCase):
 
     def test_non_namespaced(self):
         cmd = self.get_cmd('foo')
-        self.assertEqual(cmd._repo_name, 'foo')
-        self.assertEqual(cmd.ns_repo_name, 'foo')
+        self.assertEqual(cmd.package._repo_name, 'foo')
+        self.assertEqual(cmd.package.ns_repo_name, 'foo')
 
     def test_just_module_name(self):
         cmd = self.get_cmd('foo', self.conf_file)
-        self.assertEqual(cmd._repo_name, 'foo')
-        self.assertEqual(cmd.ns_repo_name, 'rpms/foo')
+        self.assertEqual(cmd.package._repo_name, 'foo')
+        self.assertEqual(cmd.package.ns_repo_name, 'rpms/foo')
 
     def test_explicit_default(self):
         cmd = self.get_cmd('rpms/foo', self.conf_file)
-        self.assertEqual(cmd._repo_name, 'foo')
-        self.assertEqual(cmd.ns_repo_name, 'rpms/foo')
+        self.assertEqual(cmd.package._repo_name, 'foo')
+        self.assertEqual(cmd.package.ns_repo_name, 'rpms/foo')
 
     def test_with_namespace(self):
         cmd = self.get_cmd('container/foo', self.conf_file)
-        self.assertEqual(cmd._repo_name, 'foo')
-        self.assertEqual(cmd.ns_repo_name, 'container/foo')
+        self.assertEqual(cmd.package._repo_name, 'foo')
+        self.assertEqual(cmd.package.ns_repo_name, 'container/foo')
 
     def test_with_nested_namespace(self):
         cmd = self.get_cmd('user/project/foo', self.conf_file)
-        self.assertEqual(cmd._repo_name, 'foo')
-        self.assertEqual(cmd.ns_repo_name, 'user/project/foo')
+        self.assertEqual(cmd.package._repo_name, 'foo')
+        self.assertEqual(cmd.package.ns_repo_name, 'user/project/foo')
 
 
 class TestKojiConfigBackwardCompatibility(CliTestCase):
@@ -476,8 +476,12 @@ class TestSrpm(CliTestCase):
             cli = self.new_cli()
             cli.srpm()
 
-        expected_cmd = ['rpmbuild'] + cli.cmd.rpmdefines + \
-            ['--nodeps', '-bs', os.path.join(cli.cmd.path, cli.cmd.spec)]
+        expected_cmd = (
+            ['rpmbuild'] +
+            cli.cmd.package.rpmdefines +
+            ['--nodeps', '-bs', os.path.join(
+                cli.cmd.path, cli.cmd.package.spec)]
+        )
         _run_command.assert_called_once_with(expected_cmd, shell=True)
 
 
@@ -491,8 +495,8 @@ class TestCompile(CliTestCase):
             cli = self.new_cli()
             cli.compile()
 
-        spec = os.path.join(cli.cmd.path, cli.cmd.spec)
-        rpmbuild = ['rpmbuild'] + cli.cmd.rpmdefines + ['-bc', spec]
+        spec = os.path.join(cli.cmd.path, cli.cmd.package.spec)
+        rpmbuild = ['rpmbuild'] + cli.cmd.package.rpmdefines + ['-bc', spec]
 
         _run_command.assert_called_once_with(rpmbuild, shell=True)
 
@@ -507,8 +511,8 @@ class TestCompile(CliTestCase):
             cli = self.new_cli()
             cli.compile()
 
-        spec = os.path.join(cli.cmd.path, cli.cmd.spec)
-        rpmbuild = ['rpmbuild'] + cli.cmd.rpmdefines + \
+        spec = os.path.join(cli.cmd.path, cli.cmd.package.spec)
+        rpmbuild = ['rpmbuild'] + cli.cmd.package.rpmdefines + \
             ["--define '_builddir %s'" % builddir, '--target', 'i686', '--short-circuit',
              '--nocheck', '--quiet', '-bc', spec]
 
@@ -525,8 +529,8 @@ class TestPrep(CliTestCase):
             cli = self.new_cli()
             cli.prep()
 
-        spec = os.path.join(cli.cmd.path, cli.cmd.spec)
-        rpmbuild = ['rpmbuild'] + cli.cmd.rpmdefines + ['--nodeps', '-bp', spec]
+        spec = os.path.join(cli.cmd.path, cli.cmd.package.spec)
+        rpmbuild = ['rpmbuild'] + cli.cmd.package.rpmdefines + ['--nodeps', '-bp', spec]
         _run_command.assert_called_once_with(rpmbuild, shell=True)
 
     @patch('pyrpkg.Commands._run_command')
@@ -540,8 +544,8 @@ class TestPrep(CliTestCase):
             cli = self.new_cli()
             cli.prep()
 
-        spec = os.path.join(cli.cmd.path, cli.cmd.spec)
-        rpmbuild = ['rpmbuild'] + cli.cmd.rpmdefines + \
+        spec = os.path.join(cli.cmd.path, cli.cmd.package.spec)
+        rpmbuild = ['rpmbuild'] + cli.cmd.package.rpmdefines + \
             ["--define '_builddir %s'" % builddir, '--target', 'i686', '--quiet', '--nodeps',
              '-bp', spec]
         _run_command.assert_called_once_with(rpmbuild, shell=True)
@@ -557,8 +561,8 @@ class TestInstall(CliTestCase):
             cli = self.new_cli()
             cli.install()
 
-        spec = os.path.join(cli.cmd.path, cli.cmd.spec)
-        rpmbuild = ['rpmbuild'] + cli.cmd.rpmdefines + ['-bi', spec]
+        spec = os.path.join(cli.cmd.path, cli.cmd.package.spec)
+        rpmbuild = ['rpmbuild'] + cli.cmd.package.rpmdefines + ['-bi', spec]
 
         _run_command.assert_called_once_with(rpmbuild, shell=True)
 
@@ -573,8 +577,8 @@ class TestInstall(CliTestCase):
             cli = self.new_cli()
             cli.install()
 
-        spec = os.path.join(cli.cmd.path, cli.cmd.spec)
-        rpmbuild = ['rpmbuild'] + cli.cmd.rpmdefines + \
+        spec = os.path.join(cli.cmd.path, cli.cmd.package.spec)
+        rpmbuild = ['rpmbuild'] + cli.cmd.package.rpmdefines + \
             ["--define '_builddir %s'" % builddir, '--target', 'i686', '--nocheck', '--quiet',
              '-bi', spec]
 
@@ -591,9 +595,10 @@ class TestLocal(CliTestCase):
             cli = self.new_cli()
             cli.local()
 
-        spec = os.path.join(cli.cmd.path, cli.cmd.spec)
-        rpmbuild = ['rpmbuild'] + cli.cmd.rpmdefines + ['-ba', spec]
-        tee = ['tee', '.build-%s-%s.log' % (cli.cmd.ver, cli.cmd.rel)]
+        spec = os.path.join(cli.cmd.path, cli.cmd.package.spec)
+        rpmbuild = ['rpmbuild'] + cli.cmd.package.rpmdefines + ['-ba', spec]
+        tee = ['tee', '.build-%s-%s.log' % (cli.cmd.package.ver,
+                                            cli.cmd.package.rel)]
         cmd = '%s | %s; exit "${PIPESTATUS[0]} ${pipestatus[1]}"' % (
             ' '.join(rpmbuild), ' '.join(tee)
         )
@@ -610,11 +615,12 @@ class TestLocal(CliTestCase):
             cli = self.new_cli()
             cli.local()
 
-        spec = os.path.join(cli.cmd.path, cli.cmd.spec)
-        rpmbuild = ['rpmbuild'] + cli.cmd.rpmdefines + \
+        spec = os.path.join(cli.cmd.path, cli.cmd.package.spec)
+        rpmbuild = ['rpmbuild'] + cli.cmd.package.rpmdefines + \
             ['--with', 'a', '--without', 'b', "--define '_builddir %s'" % builddir,
              '--target', 'i686', '--quiet', '-ba', spec]
-        tee = ['tee', '.build-%s-%s.log' % (cli.cmd.ver, cli.cmd.rel)]
+        tee = ['tee', '.build-%s-%s.log' % (cli.cmd.package.ver,
+                                            cli.cmd.package.rel)]
 
         cmd = '%s | %s; exit "${PIPESTATUS[0]} ${pipestatus[1]}"' % (
             ' '.join(rpmbuild), ' '.join(tee)
@@ -633,8 +639,8 @@ class TestVerifyFiles(CliTestCase):
             cli = self.new_cli()
             cli.verify_files()
 
-        spec = os.path.join(cli.cmd.path, cli.cmd.spec)
-        rpmbuild = ['rpmbuild'] + cli.cmd.rpmdefines + ['-bl', spec]
+        spec = os.path.join(cli.cmd.path, cli.cmd.package.spec)
+        rpmbuild = ['rpmbuild'] + cli.cmd.package.rpmdefines + ['-bl', spec]
         _run_command.assert_called_once_with(rpmbuild, shell=True)
 
     @patch('pyrpkg.Commands._run_command')
@@ -648,8 +654,8 @@ class TestVerifyFiles(CliTestCase):
             cli = self.new_cli()
             cli.verify_files()
 
-        spec = os.path.join(cli.cmd.path, cli.cmd.spec)
-        rpmbuild = ['rpmbuild'] + cli.cmd.rpmdefines + \
+        spec = os.path.join(cli.cmd.path, cli.cmd.package.spec)
+        rpmbuild = ['rpmbuild'] + cli.cmd.package.rpmdefines + \
             ["--define '_builddir %s'" % builddir, '--quiet', '-bl', spec]
         _run_command.assert_called_once_with(rpmbuild, shell=True)
 
@@ -875,7 +881,7 @@ class TestLint(CliTestCase):
             cli = self.new_cli()
             cli.lint()
 
-        rpmlint = ['rpmlint', os.path.join(cli.cmd.path, cli.cmd.spec)]
+        rpmlint = ['rpmlint', os.path.join(cli.cmd.path, cli.cmd.package.spec)]
         _run_command.assert_called_once_with(rpmlint, shell=True)
 
     @patch('pyrpkg.Commands._run_command')
@@ -889,7 +895,7 @@ class TestLint(CliTestCase):
             cli = self.new_cli()
             cli.lint()
 
-        rpmlint = ['rpmlint', '-i', os.path.join(cli.cmd.path, cli.cmd.spec)]
+        rpmlint = ['rpmlint', '-i', os.path.join(cli.cmd.path, cli.cmd.package.spec)]
         _run_command.assert_called_once_with(rpmlint, shell=True)
 
     @patch('pyrpkg.Commands._run_command')
@@ -905,7 +911,10 @@ class TestLint(CliTestCase):
             cli = self.new_cli()
             cli.lint()
 
-        rpmlint = ['rpmlint', '-f', lint_config_path, os.path.join(cli.cmd.path, cli.cmd.spec)]
+        rpmlint = [
+            'rpmlint', '-f', lint_config_path,
+            os.path.join(cli.cmd.path, cli.cmd.package.spec)
+        ]
         _run_command.assert_called_once_with(rpmlint, shell=True)
 
     @patch('pyrpkg.Commands._run_command')
@@ -923,7 +932,10 @@ class TestLint(CliTestCase):
             cli = self.new_cli()
             cli.lint()
 
-        rpmlint = ['rpmlint', '-f', lint_config_path, os.path.join(cli.cmd.path, cli.cmd.spec)]
+        rpmlint = [
+            'rpmlint', '-f', lint_config_path,
+            os.path.join(cli.cmd.path, cli.cmd.package.spec)
+        ]
         _run_command.assert_called_once_with(rpmlint, shell=True)
 
     @patch('pyrpkg.Commands._run_command')
@@ -942,8 +954,10 @@ class TestLint(CliTestCase):
             cli = self.new_cli()
             cli.lint()
 
-        rpmlint = ['rpmlint', '-f', custom_lint_config_path, os.path.join(cli.cmd.path,
-                   cli.cmd.spec)]
+        rpmlint = [
+            'rpmlint', '-f', custom_lint_config_path,
+            os.path.join(cli.cmd.path, cli.cmd.package.spec)
+        ]
         _run_command.assert_called_once_with(rpmlint, shell=True)
 
 
@@ -1359,7 +1373,8 @@ class TestMockbuild(CliTestCase):
 
     @patch('pyrpkg.Commands._config_dir_basic')
     @patch('pyrpkg.Commands._config_dir_other')
-    @patch('pyrpkg.Commands.mockconfig', new_callable=PropertyMock)
+    @patch('pyrpkg.pkginfo.PackageMetadata.mockconfig',
+           new_callable=PropertyMock)
     def test_use_mock_config_got_from_koji(
             self, mockconfig, config_dir_other, config_dir_basic):
         fd, tmpfile = tempfile.mkstemp()
@@ -1406,9 +1421,10 @@ class TestCoprBuild(CliTestCase):
 
     def setUp(self):
         super(TestCoprBuild, self).setUp()
-        self.nvr_patcher = patch('pyrpkg.Commands.nvr',
-                                 new_callable=PropertyMock,
-                                 return_value='rpkg-1.29-3.fc26')
+        self.nvr_patcher = patch(
+            'pyrpkg.pkginfo.PackageMetadata.nvr',
+            new_callable=PropertyMock,
+            return_value='rpkg-1.29-3.fc26')
         self.mock_nvr = self.nvr_patcher.start()
 
         self.srpm_patcher = patch('pyrpkg.cli.cliClient.srpm')
@@ -1472,23 +1488,27 @@ class TestMockConfig(CliTestCase):
                                     return_value='http://localhost/hub')
         self.mock_topurl = self.topurl_patcher.start()
 
-        self.disttag_patcher = patch('pyrpkg.Commands.disttag',
-                                     new_callable=PropertyMock,
-                                     return_value='fc26')
+        self.disttag_patcher = patch(
+            'pyrpkg.pkginfo.PackageMetadata.disttag',
+            new_callable=PropertyMock,
+            return_value='fc26')
         self.mock_disttag = self.disttag_patcher.start()
 
-        self.target_patcher = patch('pyrpkg.Commands.target',
-                                    new_callable=PropertyMock,
-                                    return_value='f26-candidate')
+        self.target_patcher = patch(
+            'pyrpkg.pkginfo.PackageMetadata.target',
+            new_callable=PropertyMock,
+            return_value='f26-candidate')
         self.mock_target = self.target_patcher.start()
 
-        self.localarch_patcher = patch('pyrpkg.Commands.localarch',
-                                       new_callable=PropertyMock,
-                                       return_value='x86_64')
+        self.localarch_patcher = patch(
+            'pyrpkg.pkginfo.PackageMetadata.localarch',
+            new_callable=PropertyMock,
+            return_value='x86_64')
         self.mock_localarch = self.localarch_patcher.start()
 
-        self.genMockConfig_patcher = patch('koji.genMockConfig',
-                                           return_value='x86_64 mock config')
+        self.genMockConfig_patcher = patch(
+            'koji.genMockConfig',
+            return_value='x86_64 mock config')
         self.mock_genMockConfig = self.genMockConfig_patcher.start()
 
         self.fake_build_target = {
@@ -1675,14 +1695,16 @@ class TestPatch(CliTestCase):
         self.Popen_patcher = patch('subprocess.Popen')
         self.mock_Popen = self.Popen_patcher.start()
 
-        self.repo_name_patcher = patch('pyrpkg.Commands.repo_name',
-                                       new_callable=PropertyMock,
-                                       return_value='docpkg')
+        self.repo_name_patcher = patch(
+            'pyrpkg.pkginfo.PackageMetadata.repo_name',
+            new_callable=PropertyMock,
+            return_value='docpkg')
         self.mock_repo_name = self.repo_name_patcher.start()
 
-        self.ver_patcher = patch('pyrpkg.Commands.ver',
-                                 new_callable=PropertyMock,
-                                 return_value='2.0')
+        self.ver_patcher = patch(
+            'pyrpkg.pkginfo.PackageMetadata.ver',
+            new_callable=PropertyMock,
+            return_value='2.0')
         self.mock_ver = self.ver_patcher.start()
 
     def tearDown(self):
@@ -1712,8 +1734,8 @@ class TestPatch(CliTestCase):
                 cli.patch()
                 m.return_value.write.assert_called_once_with('+ diff')
 
-        patch_file = '{0}-{1}-fix.patch'.format(cli.cmd.module_name,
-                                                cli.cmd.ver)
+        patch_file = '{0}-{1}-fix.patch'.format(cli.cmd.package.module_name,
+                                                cli.cmd.package.ver)
         self.mock_repo.return_value.repo.index.add.assert_called_once_with(
                 [patch_file])
 
@@ -1748,8 +1770,8 @@ class TestPatch(CliTestCase):
         with patch('sys.argv', new=cli_cmd):
             cli = self.new_cli()
 
-            patch_file = '{0}-{1}-fix.patch'.format(cli.cmd.module_name,
-                                                    cli.cmd.ver)
+            patch_file = '{0}-{1}-fix.patch'.format(cli.cmd.package.module_name,
+                                                    cli.cmd.package.ver)
             copied_patch_file = '{0}~'.format(patch_file)
 
             with patch.object(six.moves.builtins, 'open',
@@ -1788,8 +1810,8 @@ class TestPatch(CliTestCase):
         with patch('sys.argv', new=cli_cmd):
             cli = self.new_cli()
 
-            patch_file = '{0}-{1}-fix.patch'.format(cli.cmd.module_name,
-                                                    cli.cmd.ver)
+            patch_file = '{0}-{1}-fix.patch'.format(cli.cmd.package.module_name,
+                                                    cli.cmd.package.ver)
             with patch('os.path.exists', return_value=False) as exists:
                 six.assertRaisesRegex(
                     self, rpkgError,
@@ -2341,7 +2363,7 @@ State:    failed
         mock_run.return_value = mock_proc
         with patch('sys.argv', new=cli_cmd):
             cli = self.new_cli()
-            file_path = os.path.join(self.cloned_repo_path, cli.cmd.module_name + '.yaml')
+            file_path = os.path.join(self.cloned_repo_path, cli.cmd.package.module_name + '.yaml')
             # we create an empty file for the purpose of this test so we don't raise an exception
             open(file_path, 'a').close()
             cli.module_build_local()
@@ -2367,7 +2389,7 @@ State:    failed
             cli = self.new_cli()
             cli.config.set("rpkg.mbs", "config_file", "/etc/module-build-service/custom.py")
             cli.config.set("rpkg.mbs", "config_section", "CustomSection")
-            file_path = os.path.join(self.cloned_repo_path, cli.cmd.module_name + '.yaml')
+            file_path = os.path.join(self.cloned_repo_path, cli.cmd.package.repo_name + '.yaml')
             # we create an empty file for the purpose of this test so we don't raise an exception
             open(file_path, 'a').close()
             cli.module_build_local()
@@ -2537,7 +2559,7 @@ State:    failed
             # we create an empty file for the purpose of this test so we don't
             # raise an exception
             modulemd_yml = os.path.join(self.cloned_repo_path,
-                                        cli.cmd.module_name + '.yaml')
+                                        cli.cmd.package.module_name + '.yaml')
             self.write_file(modulemd_yml)
 
             six.assertRaisesRegex(self, rpkgError, r'mbs-manager is missing.+',
@@ -2599,8 +2621,8 @@ class TestOptionNameAndNamespace(CliTestCase):
         ]
         with patch('sys.argv', new=cli):
             cli = self.new_cli()
-            self.assertEqual('somepkg', cli.cmd.module_name)
-            self.assertEqual('rpms', cli.cmd.ns)
+            self.assertEqual('somepkg', cli.cmd.package.repo_name)
+            self.assertEqual('rpms', cli.cmd.package.ns)
 
     def test_name_and_namespace_with_distgit_namespace_disabled(self):
         cli = [
@@ -2609,8 +2631,8 @@ class TestOptionNameAndNamespace(CliTestCase):
         ]
         with patch('sys.argv', new=cli):
             cli = self.new_cli()
-            self.assertEqual('somepkg', cli.cmd.module_name)
-            self.assertEqual('rpms', cli.cmd.ns)
+            self.assertEqual('somepkg', cli.cmd.package.repo_name)
+            self.assertEqual('rpms', cli.cmd.package.ns)
 
     def test_name_and_namespace_with_distgit_namespace_enabled(self):
         cli = [
@@ -2620,5 +2642,5 @@ class TestOptionNameAndNamespace(CliTestCase):
         with patch('sys.argv', new=cli):
             abs_filename = self.get_absolute_conf_filename('rpkg-ns.conf')
             cli = self.new_cli(abs_filename)
-            self.assertEqual('somepkg', cli.cmd.module_name)
-            self.assertEqual('modules', cli.cmd.ns)
+            self.assertEqual('somepkg', cli.cmd.package.repo_name)
+            self.assertEqual('modules', cli.cmd.package.ns)
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 36321e2..9eb12fa 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -5,11 +5,9 @@ import io
 import os
 import shutil
 import six
-import subprocess
 import tempfile
 
 import git
-import rpm
 from mock import call
 from mock import patch
 from mock import Mock
@@ -21,181 +19,6 @@ from pyrpkg import rpkgError
 from utils import CommandTestCase
 
 
-def mock_load_rpmdefines(self):
-    """Mock Commands.load_rpmdefines by setting empty list to _rpmdefines
-
-    :param Commands self: load_rpmdefines is an instance method of Commands,
-    self is the instance whish is calling this method.
-    """
-    self._rpmdefines = []
-
-
-def mock_load_spec(fake_spec):
-    """Return a mocked load_spec method that sets a fake spec to Commands
-
-    :param str fake_spec: an arbitrary string representing a fake spec
-    file. What value is passed to fake_spec depends on the test purpose
-    completely.
-    """
-    def mocked_load_spec(self):
-        """Mocked load_spec to set fake spec to an instance of Commands
-
-        :param Commands self: load_spec is an instance method of Commands, self
-        is the instance which is calling this method.
-        """
-        self._spec = fake_spec
-    return mocked_load_spec
-
-
-class LoadNameVerRelTest(CommandTestCase):
-    """Test case for Commands.load_nameverrel"""
-
-    def setUp(self):
-        super(LoadNameVerRelTest, self).setUp()
-        self.cmd = self.make_commands()
-        self.checkout_branch(self.cmd.repo, 'eng-rhel-6')
-        self.tempdir = tempfile.mkdtemp(prefix='rpkg_test_')
-
-    def tearDown(self):
-        super(LoadNameVerRelTest, self).tearDown()
-        shutil.rmtree(self.tempdir)
-
-    def test_load_from_spec(self):
-        """Ensure name, version, release can be loaded from a valid SPEC"""
-        self.cmd.load_nameverrel()
-        self.assertEqual('docpkg', self.cmd._package_name_spec)
-        self.assertEqual('0', self.cmd._epoch)
-        self.assertEqual('1.2', self.cmd._ver)
-        self.assertEqual('2.el6', self.cmd._rel)
-
-    def test_load_spec_where_path_contains_space(self):
-        """Ensure load_nameverrel works with a repo whose path contains space
-
-        This test aims to test the space appearing in path does not break rpm
-        command execution.
-
-        For this test purpose, firstly, original repo has to be cloned to a
-        new place which has a name containing arbitrary spaces.
-        """
-        cloned_repo_dir = os.path.join(self.tempdir, 'rpkg test cloned repo')
-        if os.path.exists(cloned_repo_dir):
-            shutil.rmtree(cloned_repo_dir)
-        cloned_repo = self.cmd.repo.repo.clone(cloned_repo_dir)
-
-        # Switching to branch eng-rhel-6 explicitly is required by running this
-        # on RHEL6/7 because an old version of git is available in the
-        # repo.
-        # The failure reason is, old version of git makes the master as the
-        # active branch in cloned repository, whatever the current active
-        # branch is in the remote repository.
-        # As of fixing this, I ran test on Fedora 23 with git 2.5.5, and test
-        # fails on RHEL7 with git 1.8.3.1
-        cloned_repo.git.checkout('eng-rhel-6')
-
-        cmd = self.make_commands(path=cloned_repo_dir)
-
-        cmd.load_nameverrel()
-        self.assertEqual('docpkg', cmd._package_name_spec)
-        self.assertEqual('0', cmd._epoch)
-        self.assertEqual('1.2', cmd._ver)
-        self.assertEqual('2.el6', cmd._rel)
-
-    @patch('pyrpkg.Commands.load_rpmdefines', new=mock_load_rpmdefines)
-    @patch('pyrpkg.Commands.load_spec',
-           new=mock_load_spec('unknown-rpm-option a-nonexistent-package.spec'))
-    def test_load_when_rpm_fails(self):
-        """Ensure rpkgError is raised when rpm command fails
-
-        Commands.load_spec is mocked to help generate an incorrect rpm command
-        line to cause the error that this test expects.
-
-        Test test does not care about what rpm defines are retrieved from
-        repository, so setting an empty list to Commands._rpmdefines is safe
-        and enough.
-        """
-        self.assertRaises(rpkgError, self.cmd.load_nameverrel)
-
-    def test_load_when_echo_text_from_spec(self):
-        import utils
-        self.write_file(os.path.join(self.cloned_repo_path, self.spec_file),
-                        content=utils.spec_file_echo_text)
-
-        self.cmd.load_nameverrel()
-        self.assertEqual('docpkg', self.cmd._package_name_spec)
-        self.assertEqual('0', self.cmd._epoch)
-        self.assertEqual('1.2', self.cmd._ver)
-        self.assertEqual('2.el6', self.cmd._rel)
-
-
-class LoadRPMDefinesTest(CommandTestCase):
-    """Test case for Commands.load_rpmdefines"""
-
-    def setUp(self):
-        super(LoadRPMDefinesTest, self).setUp()
-        self.cmd = self.make_commands()
-
-    def assert_loaded_rpmdefines(self, branch_name, expected_defines):
-        self.checkout_branch(self.cmd.repo, branch_name)
-
-        self.cmd.load_rpmdefines()
-        self.assertTrue(self.cmd._rpmdefines)
-
-        # Convert defines into dict for assertion conveniently. The dict
-        # contains mapping from variable name to value. For example,
-        # {
-        #     '_sourcedir': '/path/to/src-dir',
-        #     '_specdir': '/path/to/spec',
-        #     '_builddir': '/path/to/build-dir',
-        #     '_srcrpmdir': '/path/to/srcrpm-dir',
-        #     'dist': 'el7'
-        # }
-        defines = dict([item.split(' ') for item in (
-            define.replace("'", '').split(' ', 1)[1] for
-            define in self.cmd._rpmdefines)])
-
-        for var, val in expected_defines.items():
-            self.assertTrue(var in defines)
-            self.assertEqual(val, defines[var])
-
-    def test_load_rpmdefines_from_eng_rhel_6(self):
-        """Run load_rpmdefines against branch eng-rhel-6"""
-        expected_rpmdefines = {
-            '_sourcedir': self.cloned_repo_path,
-            '_specdir': self.cloned_repo_path,
-            '_builddir': self.cloned_repo_path,
-            '_srcrpmdir': self.cloned_repo_path,
-            '_rpmdir': self.cloned_repo_path,
-            'dist': u'.el6',
-            'rhel': u'6',
-            'el6': u'1',
-            }
-        self.assert_loaded_rpmdefines('eng-rhel-6', expected_rpmdefines)
-
-    def test_load_rpmdefines_from_eng_rhel_6_5(self):
-        """Run load_rpmdefines against branch eng-rhel-6.5
-
-        Working on a different branch name is the only difference from test
-        method test_load_rpmdefines_from_eng_rhel_6.
-        """
-        expected_rpmdefines = {
-            '_sourcedir': self.cloned_repo_path,
-            '_specdir': self.cloned_repo_path,
-            '_builddir': self.cloned_repo_path,
-            '_srcrpmdir': self.cloned_repo_path,
-            '_rpmdir': self.cloned_repo_path,
-            'dist': u'.el6_5',
-            'rhel': u'6',
-            'el6_5': u'1',
-            }
-        self.assert_loaded_rpmdefines('eng-rhel-6.5', expected_rpmdefines)
-
-    @patch('pyrpkg.pkgrepo.PackageRepository.branch_merge', new_callable=PropertyMock)
-    def test_load_rpmdefines_against_invalid_branch(self, branch_merge):
-        branch_merge.return_value = 'invalid-branch-name'
-
-        self.assertRaises(rpkgError, self.cmd.load_rpmdefines)
-
-
 class CheckRepoWithOrWithoutDistOptionCase(CommandTestCase):
     """Check whether there are unpushed changes with or without specified dist
 
@@ -293,117 +116,6 @@ $what_is_this
         self.assertEqual(expected_lines, clog_lines)
 
 
-class TestProperties(CommandTestCase):
-
-    def setUp(self):
-        super(TestProperties, self).setUp()
-        self.invalid_repo = tempfile.mkdtemp()
-
-    def tearDown(self):
-        shutil.rmtree(self.invalid_repo)
-        super(TestProperties, self).tearDown()
-
-    def test_target(self):
-        cmd = self.make_commands()
-        self.checkout_branch(cmd.repo, 'eng-rhel-6')
-        self.assertEqual('eng-rhel-6-candidate', cmd.target)
-
-    def test_spec(self):
-        cmd = self.make_commands()
-        self.assertEqual('docpkg.spec', cmd.spec)
-
-    def test_no_spec_as_it_is_deadpackage(self):
-        with patch('os.listdir', return_value=['dead.package']):
-            cmd = self.make_commands()
-            self.assertRaises(rpkgError, cmd.load_spec)
-
-    def test_no_spec_there(self):
-        with patch('os.listdir', return_value=['anyfile']):
-            cmd = self.make_commands()
-            self.assertRaises(rpkgError, cmd.load_spec)
-
-    def test_nvr(self):
-        cmd = self.make_commands(dist='eng-rhel-6')
-
-        module_name = os.path.basename(self.repo_path)
-        self.assertEqual('{0}-1.2-2.el6'.format(module_name), cmd.nvr)
-
-    def test_nvr_cannot_get_module_name_from_push_url(self):
-        cmd = self.make_commands(path=self.repo_path, dist='eng-rhel-6')
-        self.assertEqual('docpkg-1.2-2.el6', cmd.nvr)
-
-    def test_localarch(self):
-        expected_localarch = rpm.expandMacro('%{_arch}')
-        cmd = self.make_commands()
-        self.assertEqual(expected_localarch, cmd.localarch)
-
-    def test_commithash(self):
-        cmd = self.make_commands(path=self.cloned_repo_path)
-        repo = git.Repo(self.cloned_repo_path)
-        expected_commit_hash = str(six.next(repo.iter_commits()))
-        self.assertEqual(expected_commit_hash, cmd.repo.commit_hash)
-
-    def test_dist(self):
-        repo = git.Repo(self.cloned_repo_path)
-        self.checkout_branch(repo, 'eng-rhel-7')
-
-        cmd = self.make_commands(path=self.cloned_repo_path)
-        self.assertEqual('el7', cmd.disttag)
-        self.assertEqual('rhel', cmd.distvar)
-        self.assertEqual('7', cmd.distval)
-        self.assertEqual('0', cmd.epoch)
-
-    def test_repo(self):
-        cmd = self.make_commands(path=self.cloned_repo_path)
-        cmd.load_repo()
-        self.assertEqual(self.cloned_repo_path, os.path.dirname(cmd._repo.git_dir))
-
-        cmd = self.make_commands(path=self.invalid_repo)
-        self.assertRaises(rpkgError, cmd.load_repo)
-
-        cmd = self.make_commands(path='some-dir')
-        self.assertRaises(rpkgError, cmd.load_repo)
-
-    def test_mockconfig(self):
-        cmd = self.make_commands(path=self.cloned_repo_path)
-        self.checkout_branch(cmd.repo, 'eng-rhel-7')
-        expected_localarch = rpm.expandMacro('%{_arch}')
-        self.assertEqual('eng-rhel-7-candidate-{0}'.format(expected_localarch),
-                         cmd.mockconfig)
-
-    def test_get_ns_module_name(self):
-        tests = (
-            ('http://localhost/rpms/docpkg.git', 'docpkg'),
-            ('http://localhost/docker/docpkg.git', 'docpkg'),
-            ('http://localhost/docpkg.git', 'docpkg'),
-            ('http://localhost/rpms/docpkg', 'docpkg'),
-            )
-        for push_url, expected_ns_module_name in tests:
-            with patch('pyrpkg.pkgrepo.PackageRepository.push_url',
-                       new_callable=PropertyMock,
-                       return_value=push_url):
-                cmd = self.make_commands(path=self.cloned_repo_path)
-                cmd.load_ns()
-                cmd.load_module_name()
-                self.assertEqual(expected_ns_module_name, cmd.ns_module_name)
-
-        tests = (
-            ('http://localhost/rpms/docpkg.git', 'rpms/docpkg'),
-            ('http://localhost/docker/docpkg.git', 'docker/docpkg'),
-            ('http://localhost/docpkg.git', 'rpms/docpkg'),
-            ('http://localhost/rpms/docpkg', 'rpms/docpkg'),
-            )
-        for push_url, expected_ns_module_name in tests:
-            with patch('pyrpkg.pkgrepo.PackageRepository.push_url',
-                       new_callable=PropertyMock,
-                       return_value=push_url):
-                cmd = self.make_commands(path=self.cloned_repo_path)
-                cmd.distgit_namespaced = True
-                cmd.load_ns()
-                cmd.load_module_name()
-                self.assertEqual(expected_ns_module_name, cmd.ns_module_name)
-
-
 class TestNamespaced(CommandTestCase):
 
     def test_get_namespace_giturl(self):
@@ -505,28 +217,6 @@ class TestTagInheritanceTag(CommandTestCase):
         self.assertRaises(rpkgError, cmd.check_inheritance, build_target, dest_tag)
 
 
-class TestLoadModuleNameFromSpecialPushURL(CommandTestCase):
-    """Test load module name from a special push url that ends in /
-
-    For issue: https://pagure.io/rpkg/issue/192
-    """
-
-    def setUp(self):
-        super(TestLoadModuleNameFromSpecialPushURL, self).setUp()
-
-        self.case_repo = tempfile.mkdtemp(prefix='case-test-load-module-name-')
-        cmd = ['git', 'clone', '{0}/'.format(self.repo_path), self.case_repo]
-        self.run_cmd(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-
-    def tearDown(self):
-        shutil.rmtree(self.case_repo)
-        super(TestLoadModuleNameFromSpecialPushURL, self).tearDown()
-
-    def test_load_module_name(self):
-        cmd = self.make_commands(path=self.case_repo)
-        self.assertEqual(os.path.basename(self.repo_path), cmd.module_name)
-
-
 class TestLoginKojiSession(CommandTestCase):
     """Test login_koji_session"""
 
@@ -620,7 +310,7 @@ class TestLoginKojiSession(CommandTestCase):
 class TestConstructBuildURL(CommandTestCase):
     """Test Commands.construct_build_url"""
 
-    @patch('pyrpkg.Commands.ns_repo_name', new_callable=PropertyMock)
+    @patch('pyrpkg.PackageMetadata.ns_repo_name', new_callable=PropertyMock)
     @patch('pyrpkg.PackageRepository.commit_hash', new_callable=PropertyMock)
     def test_construct_url(self, commit_hash, ns_module_name):
         commit_hash.return_value = '12345'
@@ -702,7 +392,7 @@ class TestConfigMockConfigDir(CommandTestCase):
 
         self.fake_root = 'fedora-26-x86_64'
         self.mock_config_patcher = patch('pyrpkg.Commands.mock_config',
-                                         return_value='mock config x86_64')
+                                         return_value=u'mock config x86_64')
         self.mock_mock_config = self.mock_config_patcher.start()
 
         self.mkdtemp_patcher = patch('tempfile.mkdtemp',
@@ -739,7 +429,8 @@ class TestConfigMockConfigDir(CommandTestCase):
             self.assertEqual(self.mock_mock_config.return_value,
                              f.read().strip())
 
-    @patch('pyrpkg.Commands.mockconfig', new_callable=PropertyMock)
+    @patch('pyrpkg.pkginfo.PackageMetadata.mockconfig',
+           new_callable=PropertyMock)
     def test_config_using_root_guessed_from_branch(self, mockconfig):
         mockconfig.return_value = 'f26-candidate-i686'
 
@@ -845,13 +536,18 @@ class TestConfigMockConfigDirWithNecessaryFiles(CommandTestCase):
 
 
 class TestLint(CommandTestCase):
+
     @patch('glob.glob')
     @patch('os.path.exists')
     @patch('pyrpkg.Commands._run_command')
-    @patch('pyrpkg.Commands.load_rpmdefines', new=mock_load_rpmdefines)
-    @patch('pyrpkg.Commands.rel', new_callable=PropertyMock)
-    def test_lint_each_file_once(self, rel, run, exists, glob):
+    @patch('pyrpkg.PackageMetadata.repo_name', new_callable=PropertyMock)
+    @patch('pyrpkg.PackageMetadata.rpmdefines', new_callable=PropertyMock)
+    @patch('pyrpkg.PackageMetadata.rel', new_callable=PropertyMock)
+    def test_lint_each_file_once(
+            self, rel, rpmdefines, module_name, run, exists, glob):
         rel.return_value = '2.fc26'
+        rpmdefines.return_value = []
+        module_name.return_value = 'docpkg'
 
         cmd = self.make_commands()
         srpm_path = os.path.join(cmd.path, 'docpkg-1.2-2.fc26.src.rpm')
@@ -861,6 +557,10 @@ class TestLint(CommandTestCase):
             return path in [
                 srpm_path,
                 os.path.join(cmd.path, 'x86_64'),
+
+                # For checking existence of repo directory when visit property
+                # self.repo
+                self.cloned_repo_path,
             ]
 
         def _mock_glob(g):
diff --git a/tests/test_pkg_metadata.py b/tests/test_pkg_metadata.py
new file mode 100644
index 0000000..0bf4164
--- /dev/null
+++ b/tests/test_pkg_metadata.py
@@ -0,0 +1,269 @@
+# -*- coding: utf-8 -*-
+
+import os
+import rpm
+import shutil
+import subprocess
+import tempfile
+
+from mock import patch, PropertyMock
+from utils import CommandTestCase
+from pyrpkg.errors import rpkgError
+from pyrpkg.pkginfo import PackageMetadata
+from pyrpkg.pkgrepo import PackageRepository
+
+
+class TestProperties(CommandTestCase):
+    """Test various package properties"""
+
+    def setUp(self):
+        super(TestProperties, self).setUp()
+        self.repo = PackageRepository(self.cloned_repo_path)
+        self.package = PackageMetadata(self.repo)
+        self.invalid_repo = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.invalid_repo)
+        super(TestProperties, self).tearDown()
+
+    def test_target(self):
+        self.repo.git.checkout('eng-rhel-6')
+        self.assertEqual('eng-rhel-6-candidate', self.package.target)
+
+    def test_spec(self):
+        self.assertEqual('docpkg.spec', self.package.spec)
+
+    def test_no_spec_as_it_is_deadpackage(self):
+        with patch('os.listdir', return_value=['dead.package']):
+            self.assertRaises(rpkgError, self.package.load_spec)
+
+    def test_no_spec_there(self):
+        with patch('os.listdir', return_value=['anyfile']):
+            self.assertRaises(rpkgError, self.package.load_spec)
+
+    def test_nvr(self):
+        repo = PackageRepository(self.cloned_repo_path,
+                                 overwritten_branch_merge='eng-rhel-6')
+        package = PackageMetadata(repo)
+
+        module_name = os.path.basename(self.repo_path)
+        self.assertEqual('{0}-1.2-2.el6'.format(module_name), package.nvr)
+
+    def test_nvr_cannot_get_module_name_from_push_url(self):
+        repo = PackageRepository(self.repo_path,
+                                 overwritten_branch_merge='eng-rhel-6')
+        package = PackageMetadata(repo)
+        self.assertEqual('docpkg-1.2-2.el6', package.nvr)
+
+    def test_localarch(self):
+        expected_localarch = rpm.expandMacro('%{_arch}')
+        self.assertEqual(expected_localarch, self.package.localarch)
+
+
+class TestNamespacedModuleName(CommandTestCase):
+    """Test PackageMetadata.ns_module_name"""
+
+    def setUp(self):
+        super(TestNamespacedModuleName, self).setUp()
+        self.repo = PackageRepository(self.cloned_repo_path)
+
+    def test_distgit_namespaced_is_disabled(self):
+        tests = (
+            ('http://localhost/rpms/docpkg.git', 'docpkg'),
+            ('http://localhost/docker/docpkg.git', 'docpkg'),
+            ('http://localhost/docpkg.git', 'docpkg'),
+            ('http://localhost/rpms/docpkg', 'docpkg'),
+            )
+        for push_url, expected_ns_module_name in tests:
+            with patch('pyrpkg.pkgrepo.PackageRepository.push_url',
+                       new_callable=PropertyMock,
+                       return_value=push_url):
+                package = PackageMetadata(self.repo, distgit_namespaced=False)
+                package.load_ns()
+                package.load_module_name()
+                self.assertEqual(
+                    expected_ns_module_name, package.ns_module_name)
+
+    def test_distgit_namedspaced_is_enabled(self):
+        tests = (
+            ('http://localhost/rpms/docpkg.git', 'rpms/docpkg'),
+            ('http://localhost/docker/docpkg.git', 'docker/docpkg'),
+            ('http://localhost/docpkg.git', 'rpms/docpkg'),
+            ('http://localhost/rpms/docpkg', 'rpms/docpkg'),
+            )
+        for push_url, expected_ns_module_name in tests:
+            with patch('pyrpkg.pkgrepo.PackageRepository.push_url',
+                       new_callable=PropertyMock,
+                       return_value=push_url):
+                package = PackageMetadata(self.repo, distgit_namespaced=True)
+                package.load_ns()
+                package.load_module_name()
+                self.assertEqual(
+                    expected_ns_module_name, package.ns_module_name)
+
+
+class LoadNameVerRelTest(CommandTestCase):
+    """Test case for Commands.load_nameverrel"""
+
+    def setUp(self):
+        super(LoadNameVerRelTest, self).setUp()
+        self.repo = PackageRepository(self.cloned_repo_path)
+        self.package = PackageMetadata(self.repo)
+        self.repo.git.checkout('eng-rhel-6')
+        self.tempdir = tempfile.mkdtemp(prefix='rpkg_test_')
+
+    def tearDown(self):
+        super(LoadNameVerRelTest, self).tearDown()
+        shutil.rmtree(self.tempdir)
+
+    def test_load_from_spec(self):
+        """Ensure name, version, release can be loaded from a valid SPEC"""
+        self.package.load_nameverrel()
+        self.assertEqual('docpkg', self.package._package_name_spec)
+        self.assertEqual('0', self.package._epoch)
+        self.assertEqual('1.2', self.package._ver)
+        self.assertEqual('2.el6', self.package._rel)
+
+    def test_load_spec_where_path_contains_space(self):
+        """Ensure load_nameverrel works with a repo whose path contains space
+
+        This test aims to test the space appearing in path does not break rpm
+        command execution.
+
+        For this test purpose, firstly, original repo has to be cloned to a
+        new place which has a name containing arbitrary spaces.
+        """
+        cloned_repo_dir = os.path.join(self.tempdir, 'rpkg test cloned repo')
+        if os.path.exists(cloned_repo_dir):
+            shutil.rmtree(cloned_repo_dir)
+        cloned_repo = self.repo.repo.clone(cloned_repo_dir)
+
+        # Switching to branch eng-rhel-6 explicitly is required by running this
+        # on RHEL6/7 because an old version of git is available in the
+        # repo.
+        # The failure reason is, old version of git makes the master as the
+        # active branch in cloned repository, whatever the current active
+        # branch is in the remote repository.
+        # As of fixing this, I ran test on Fedora 23 with git 2.5.5, and test
+        # fails on RHEL7 with git 1.8.3.1
+        cloned_repo.git.checkout('eng-rhel-6')
+
+        package = PackageMetadata(PackageRepository(cloned_repo_dir))
+        package.load_nameverrel()
+
+        self.assertEqual('docpkg', package._package_name_spec)
+        self.assertEqual('0', package._epoch)
+        self.assertEqual('1.2', package._ver)
+        self.assertEqual('2.el6', package._rel)
+
+    @patch('pyrpkg.pkginfo.PackageMetadata.rpmdefines', new_callable=PropertyMock)
+    @patch('pyrpkg.pkginfo.PackageMetadata.spec', new_callable=PropertyMock)
+    def test_load_when_rpm_fails(self, spec, rpmdefines):
+        """Ensure rpkgError is raised when rpm command fails
+
+        Commands.load_spec is mocked to help generate an incorrect rpm command
+        line to cause the error that this test expects.
+
+        Test test does not care about what rpm defines are retrieved from
+        repository, so setting an empty list to Commands._rpmdefines is safe
+        and enough.
+        """
+        spec.return_value = 'unknown-rpm-option a-nonexistent-package.spec'
+        # For this test, no need of any rpmdefines
+        rpmdefines.return_value = []
+        self.assertRaises(rpkgError, self.package.load_nameverrel)
+
+
+class LoadRPMDefinesTest(CommandTestCase):
+    """Test case for Commands.load_rpmdefines"""
+
+    def setUp(self):
+        super(LoadRPMDefinesTest, self).setUp()
+        self.package = PackageMetadata(
+            PackageRepository(self.cloned_repo_path))
+
+    def assert_loaded_rpmdefines(self, branch_name, expected_defines):
+        self.package.repo.git.checkout(branch_name)
+
+        self.package.load_rpmdefines()
+        self.assertTrue(self.package._rpmdefines)
+
+        # Convert defines into dict for assertion conveniently. The dict
+        # contains mapping from variable name to value. For example,
+        # {
+        #     '_sourcedir': '/path/to/src-dir',
+        #     '_specdir': '/path/to/spec',
+        #     '_builddir': '/path/to/build-dir',
+        #     '_srcrpmdir': '/path/to/srcrpm-dir',
+        #     'dist': 'el7'
+        # }
+        defines = dict([
+            item.split(' ')
+            for item in (define.replace("'", '').split(' ', 1)[1]
+                         for define in self.package._rpmdefines)
+        ])
+
+        for var, val in expected_defines.items():
+            self.assertTrue(var in defines)
+            self.assertEqual(val, defines[var])
+
+    def test_load_rpmdefines_from_eng_rhel_6(self):
+        """Run load_rpmdefines against branch eng-rhel-6"""
+        expected_rpmdefines = {
+            '_sourcedir': self.cloned_repo_path,
+            '_specdir': self.cloned_repo_path,
+            '_builddir': self.cloned_repo_path,
+            '_srcrpmdir': self.cloned_repo_path,
+            '_rpmdir': self.cloned_repo_path,
+            'dist': u'.el6',
+            'rhel': u'6',
+            'el6': u'1',
+            }
+        self.assert_loaded_rpmdefines('eng-rhel-6', expected_rpmdefines)
+
+    def test_load_rpmdefines_from_eng_rhel_6_5(self):
+        """Run load_rpmdefines against branch eng-rhel-6.5
+
+        Working on a different branch name is the only difference from test
+        method test_load_rpmdefines_from_eng_rhel_6.
+        """
+        expected_rpmdefines = {
+            '_sourcedir': self.cloned_repo_path,
+            '_specdir': self.cloned_repo_path,
+            '_builddir': self.cloned_repo_path,
+            '_srcrpmdir': self.cloned_repo_path,
+            '_rpmdir': self.cloned_repo_path,
+            'dist': u'.el6_5',
+            'rhel': u'6',
+            'el6_5': u'1',
+            }
+        self.assert_loaded_rpmdefines('eng-rhel-6.5', expected_rpmdefines)
+
+    @patch('pyrpkg.pkgrepo.PackageRepository.branch_merge',
+           new_callable=PropertyMock)
+    def test_load_rpmdefines_against_invalid_branch(self, branch_merge):
+        branch_merge.return_value = 'invalid-branch-name'
+        self.assertRaises(rpkgError, self.package.load_rpmdefines)
+
+
+class TestLoadModuleNameFromSpecialPushURL(CommandTestCase):
+    """Test load module name from a special push url that ends in /
+
+    For issue: https://pagure.io/rpkg/issue/192
+    """
+
+    def setUp(self):
+        super(TestLoadModuleNameFromSpecialPushURL, self).setUp()
+
+        self.case_repo = tempfile.mkdtemp(prefix='case-test-load-module-name-')
+        cmd = ['git', 'clone', '{0}/'.format(self.repo_path), self.case_repo]
+        self.run_cmd(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+    def tearDown(self):
+        shutil.rmtree(self.case_repo)
+        super(TestLoadModuleNameFromSpecialPushURL, self).tearDown()
+
+    def test_load_module_name(self):
+        repo = PackageRepository(self.case_repo)
+        package = PackageMetadata(repo)
+        self.assertEqual(os.path.basename(self.repo_path), package.module_name)
diff --git a/tests/utils.py b/tests/utils.py
index 9403a6a..8779c9a 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -167,7 +167,7 @@ class CommandTestCase(Assertions, Utils, unittest.TestCase):
             ]
         for cmd in git_cmds:
             self.run_cmd(cmd, cwd=self.repo_path,
-                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+                         stdout=subprocess.PIPE)
 
         # Clone the repo
         self.cloned_repo_path = tempfile.mkdtemp(prefix='rpkg-commands-tests-cloned-')

From 138b843e483db4b2f37653303f1c2345b2cac11f Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Jul 04 2018 06:37:46 +0000
Subject: [PATCH 2/2] Update docs for package metadata API


Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/README.rst b/README.rst
index def9b52..0ea2da4 100644
--- a/README.rst
+++ b/README.rst
@@ -20,6 +20,31 @@ and refactor in order to:
 
 .. _rpkg: https://pagure.io/rpkg
 
+
+Usage Examples
+==============
+
+Retrieve package repository information::
+
+    from pyrpkg.pkgrepo import PackageRepository
+    repo = PackageRepository('/path/to/package')
+    print(repo.branch_merge)
+    print(repo.branch_remote)
+    print(repo.push_url)
+    print(repo.commit_hash)
+
+Retrieve package metadata::
+
+    from pyrpkg.pkgrepo import PackageRepository
+    from pyrpkg.pkginfo import PackageMetadata
+
+    repo = PackageRepository('/path/to/package')
+    pkg = PackageMetadata(repo)
+    print(pkg.ns)
+    print(pkg.ns_repo_name)
+    print(pkg.disttag)
+    print(pkg.spec)
+
 License
 =======
 
diff --git a/doc/source/api.rst b/doc/source/api.rst
index 1042b23..22f275d 100644
--- a/doc/source/api.rst
+++ b/doc/source/api.rst
@@ -70,6 +70,22 @@ gitignore
    :undoc-members:
 
 
+Package Repository
+------------------
+
+.. autoclass:: pyrpkg.pkgrepo.PackageRepository
+   :members:
+   :undoc-members:
+
+
+Package Metadata
+----------------
+
+.. autoclass:: pyrpkg.pkginfo.PackageMetadata
+   :members:
+   :undoc-members:
+
+
 Utilities
 ---------
 
@@ -79,4 +95,4 @@ Utilities
 
 .. autofunction:: pyrpkg.utils.warn_deprecated
 .. autofunction:: pyrpkg.utils._log_value
-.. autofunction:: pyrpkg.utils.log_result
\ No newline at end of file
+.. autofunction:: pyrpkg.utils.log_result
diff --git a/pyrpkg/pkginfo.py b/pyrpkg/pkginfo.py
index c9bdcc5..1fd7ba2 100644
--- a/pyrpkg/pkginfo.py
+++ b/pyrpkg/pkginfo.py
@@ -22,17 +22,16 @@ logger = logging.getLogger(__name__)
 
 
 class PackageMetadata(object):
-    """Package metadata"""
+    """Package metadata
+
+    :param repo: object of
+        :class:`PackageRepository <pyrpkg.pkgrepo.PackageRepository>`
+        representing underlying git repository.
+    :type repo: :class:`PackageRepository <pyrpkg.pkgrepo.PackageRepository>`
+    :param bool distgit_namespaced: namespace is enabled in dist-git or not.
+    """
 
     def __init__(self, repo, overwrite_target=None, distgit_namespaced=True):
-        """Initialization
-
-        :param repo: object of ``PackageRepo`` representing underlying package
-            repository.
-        :type repo: :class:`PackageRepository <pyrpkg.pkgrepo.PackageRepository>`
-        :param bool distgit_namespaced: whether namespace is enabled in
-            dist-git.
-        """
         self.repo = repo
         self.distgit_namespaced = distgit_namespaced
 

