Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "get_licenses" feature (reopen #133) #201

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

130s
Copy link
Contributor

@130s 130s commented May 31, 2020

Re-opening #133 due to #133 (comment). Following description is copied from #133 with some updates.

Changes added

  • Add a feature to return a dictionary of software license and the packages that use the license.
    • Limited to only catkin package or any other type of package that rospkg can access its licenses to.
  • Add a unit test case for the added feature.

Output example

$ DOCKER_IMG=ros:melodic-ros-base && docker pull $DOCKER_IMG && docker run -it $DOCKER_IMG bash

# DIR_WS=/cws/src && mkdir -p $DIR_WS && cd $DIR_WS
# source /opt/ros/`rosversion -d`/setup.bash && git clone https://github.com/plusone-robotics/rospkg.git -b feature-license_introspection && cd rospkg 
# export PYTHONPATH=$DIR_WS/rospkg:$PYTHONPATH && export PATH=$DIR_WS/rospkg/scripts:$PATH

# apt-get update && apt-get install -y ipython && ipython
In [1]: from rospkg import RosPack
In [2]: rp = RosPack()
In [3]: rp.get_licenses('roscpp')
Out[3]: OrderedDict([(None,
              ['apr',
               'boost',
               'cmake',
               'google-mock',
               'gtest',
               'libboost-filesystem-dev',
               'libboost-thread-dev',
               'libconsole-bridge-dev',
               'log4cxx',
               'pkg-config',
               'python',
               'python-argparse',
               'python-catkin-pkg',
               'python-catkin-pkg-modules',
               'python-coverage',
               'python-empy',
               'python-mock',
               'python-nose',
               'python-rosdep-modules',
               'python-rospkg',
               'python-setuptools',
               'python-yaml',
               'python3',
               'python3-catkin-pkg',
               'python3-catkin-pkg-modules',
               'python3-coverage',
               'python3-empy',
               'python3-mock',
               'python3-nose',
               'python3-rosdep-modules',
               'python3-rospkg',
               'python3-setuptools',
               'python3-yaml',
               'tinyxml2']),
             ('Apache 2.0', ['gennodejs']),
             ('Apache License 2.0', ['ros_environment']),
             ('BSD',
              ['catkin',
               'cmake_modules',
               'cpp_common',
               'gencpp',
               'geneus',
               'genlisp',
               'genmsg',
               'genpy',
               'message_generation',
               'message_runtime',
               'rosbuild',
               'rosconsole',
               'roscpp',
               'roscpp_serialization',
               'roscpp_traits',
               'rosgraph_msgs',
               'roslang',
               'roslib',
               'rosmake',
               'rospack',
               'rostime',
               'rosunit',
               'std_msgs']),
             ('LGPL-2.1', ['xmlrpcpp'])])

Dependency on other PRs/Issues

Review item list

  • Test cases if adding new functionality/feature.
  • Would be great if we can tell at a glance that the dependency is an immediate build depend (e.g. that can be critical when it comes to L-GPL). -> Delegated to [get_depends] Add option to specify the type of dependency. #132
  • CI passing.
  • Provide how to use the new feature example.
  • Address review comments from the previous PR Add "get_licenses" method #133 -> Not all are addressed at the time of opening this PR, but in an attempt to ease the review the previous comments will be posted at the corresponding lines for unresolved review comments.
  • Code review.

Copy link
Contributor Author

@130s 130s left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dirk-thomas Not all review comments from #133 are addressed at the time of opening this PR, but in an attempt to ease the review I linked to those unresolved comments at the corresponding lines and am asking your thoughts. Thanks in advance.

.travis.yml Show resolved Hide resolved
# Traverse for Non-ROS, system packages
pkgnames_rosdep = self.get_rosdeps(package=pkg_name, implicit=implicit)
for pkgname_rosdep in pkgnames_rosdep:
license_dict[None].append(pkgname_rosdep)
Copy link
Contributor Author

@130s 130s Dec 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From #133 (comment)

Why should this function combine both - the results from get_depends as well as get_rosdeps - into a single result? That seems very use case specific.

The method get_licenses in this PR returns the dictionary of licenses and the packages. I found that useful that the output is not just a list of licenses but also the name of all packages in the dependency tree.

I found:

  • get_depends returns the list of upstream catkin packages.
  • get_rosdeps returns the list of upstream non-catkin packages.

In my usecase there's no reason to limit the scope of license introspection to catkin package (our customers want to know all the software installed on the delivered system). So including catking and non-catking packages actually important to me.

In general since the function only converts the results into a reverse mapping from license names to package names I am wondering how useful this is as part of the generic API of rospkg. Where do you plan to use the new API? Depending on the answer why can't the logic be implemented there instead?

I found this function convenient as a one-off to get all the list of licenses and all relevant packages (catkin and non-catkin packages), so that no other command is needed to get a complete list. Since in rospkg there's already get_depends, which returns a set of pkgs in the dependency tree, what get_licenses adds is essentially a license layer to the get_depends's output.

With the current implementation of this PR, capturing the licenses from the non-catkin packages is not included (e.g. what if the upstream implementation changes so that license of non-catkin package can be obtained?). I have a working code to capture licenses from system package (on Ubuntu) but due to the conversation with Dirk on previous PRs, I'm going with more granular PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to answer from a different perspective, comparing get_depends (already a part of rospkg and get_licenses method that is being added in this PR,

  • get_depends returns a list of name elements in the manifest.
  • get_licenses returns a list of name elements grouped by license in the manifest.

Because both only return elements in the manifest (package.xml) without adding any info external from the manifest file. So it makes sense to me to add get_licenses capability to rospkg.

@@ -14,9 +14,6 @@
<author>John Doe</author>
<author email="jane.doe@example.com">Jane Doe</author>

<build_depend>catkin</build_depend>
<build_depend version_gte="1.1" version_lt="2.0">genmsg</build_depend>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why were these dependencies removed in 32e6580?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the commit message, unit test fails as the dependency genmsg doesn't get installed for rospkg itself nor for the unit test. If there's a non-hacky way to install the test dependency I'm happy to add that as well as getting these lines back in.

def get_licenses(self, pkg_name, implicit=True):
"""
@summary: Return a list of licenses and the packages in the dependency tree
for the given package. Special value 'ERR' is used as the license for the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this documentation line is out of date since None is now used right?

Suggested change
for the given package. Special value 'ERR' is used as the license for the
for the given package. ``None`` is used as the license for the

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TLDR; I now set the string "license_not_found" for the packages that any license wasn't detected of.

Follow-up for #133 (comment), yes I followed the review comment and changed to 'None'. Today (on Ubuntu 20.04 with Python 3.8.5) I found it causes "TypeError ('<' not supported between instances of 'NoneType' and 'License"), and found that having None as the dict key caused that. Thus I switched back to using string. This time I'm trying to make the variable name more descriptive so hope it got better.

[get_licenses] Delegate traversing all dependency to existing method (not exactly but hopefully closer to what's suggested in ros-infrastructure#133 (comment))

[get_licenses] Add system dependency to the output.

[get_licenses] Sort licenses and pkgs.

[get_licenses] Conform to variable name convention.
…packages with multiple licenses.

Add a `licenses` field to Manifest class (in addition to `license`.

Although {manifest, package}.xml format allows to define separate xml elements, rospkg handles multiple licenses as a single string joined by a comma, which is inconvenient depending on the consumers' usecase.

Pointed out at https://github.com/ros-infrastructure/rospkg/pull/133/files#r253230420

If we want to maintain backward compatibility, `license` str field shouldn't change. So without making any change to it, this PR only adds a new field. This could cause a confusion.
I thought to add printing a warning for deprecation for license, but because it looks like `license` is (and other fields are) intended to be accessed directly (without getter method/structure), I'm not sure how to notify the users during runtime.

```
$ ipython
Python 2.7.12 (default, Nov 12 2018, 14:36:49)
Type "copyright", "credits" or "license" for more information.

IPython 2.4.1 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: import rospkg
In [2]: rp = rospkg.RosPack()
In [4]: p = rp.get_manifest("rviz")
In [5]: p.license
Out[5]: 'BSD, Creative Commons'

In [6]: p.licenses
Out[6]: ['BSD', 'Creative Commons']
```

- [x] Requester provides reproducible commands and sample test result.
- [ ] CI should pass.
- [ ] Code review.

[get_licenses] Use Manifest.licenses, not Manifest.license in order to handle pkgs with multiple licenses better.
…be usecase, and if there is, users can easily do by themselves.

[get_licenses] Call methods with kw arguments to better clarify args. Rename an arg for better clarity.

[get_licenses] Clean up.
[test][catkin] Removing a ROS package from a dependency list as it is unresolvable during the set of nosetests in this package.
Setting up apt-get for python-rosdep might be complicated. For CI pip seems to be an acceptable solution.

Enabling sudo in Travis level might be needed to run rosdep init.

Copying from bloom project how to run rosdep on Travis CI with pip.
@130s 130s force-pushed the feature-license_introspection branch 2 times, most recently from 4755c5c to 584c861 Compare December 1, 2020 04:21
…ne' causes TypeError ('<' not supported between instances of 'NoneType' and 'License) so defining non None.
@130s 130s force-pushed the feature-license_introspection branch from 584c861 to 2b64f55 Compare December 1, 2020 04:37
@130s
Copy link
Contributor Author

130s commented Dec 1, 2020

Struggling to understand why the test fails on GitHub Action for all jobs. E.g. https://github.com/ros-infrastructure/rospkg/pull/201/checks?check_run_id=1477994484#step:5:22

Locally (on a Docker container) the specific test passes (you can see the num of tests increased from 102 to 103, so the new test is run).
# python3 -m nose --with-coverage --cover-package=rospkg --with-xunit test
............../cws/src/ros-infrastructure/rospkg/src/rospkg/distro.py:195: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.
  raw_data = yaml.load(f.read())
.../cws/src/ros-infrastructure/rospkg/test/test_rospkg_distro.py:318: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.
  assert yaml.load(open(p)) == distro.raw_data, distro.raw_data
./cws/src/ros-infrastructure/rospkg/test/test_rospkg_distro.py:337: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.
  assert yaml.load(open(p)) == distro.raw_data, distro.raw_data
./cws/src/ros-infrastructure/rospkg/test/test_rospkg_distro.py:353: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.
  raw_data = yaml.load("""variants:
....................................................................................
Name                    Stmts   Miss  Cover   Missing
-----------------------------------------------------
rospkg/__init__.py          7      0   100%
rospkg/common.py           15      0   100%
rospkg/distro.py          316     12    96%   42-43, 201-204, 284, 292, 315, 624-625, 653
rospkg/environment.py      68      0   100%
rospkg/manifest.py        262     32    88%   94, 98, 113-114, 132-133, 149-150, 174, 205, 249, 292, 308, 356-362, 392, 408, 418-419, 432-433, 473-475, 486-487, 511, 513
rospkg/os_detect.py       485     94    81%   44, 57, 88, 91, 143-146, 150, 171-181, 196, 204-206, 209-224, 232, 235-237, 240-242, 269-278, 310, 408-412, 415-417, 420-422, 462-463, 466-470, 473-481, 492-493, 505, 545-546, 565, 604-606, 609-611, 796-799
rospkg/rospack.py         241     13    95%   39-40, 68-69, 279-281, 299-301, 305, 308, 519, 561-562
rospkg/stack.py           123     22    82%   68, 70-72, 97-98, 113-114, 121-124, 187, 208-209, 214, 227-229, 238, 244, 250, 257
-----------------------------------------------------
TOTAL                    1517    173    89%
----------------------------------------------------------------------
Ran 103 tests in 0.866s

OK

Or to verify the specific test is executed,

# python3 -m nose --verbose test/test_rospkg_catkin_packages.py:test_get_licenses 
Check licenses from all packages in the dependency chain ... ok

----------------------------------------------------------------------
Ran 1 test in 0.189s

OK

# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.1 LTS
Release:        20.04

@nuclearsandwich
Copy link
Contributor

I would love to see some community reviewers of this change. Especially by others who are interested in its utility.

# Traverse for Non-ROS, system packages
pkgnames_rosdep = self.get_rosdeps(package=pkg_name, implicit=implicit)
for pkgname_rosdep in pkgnames_rosdep:
license_dict[None].append(pkgname_rosdep)
Copy link
Contributor Author

@130s 130s Dec 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From #133 (comment)

Why should this function combine both - the results from get_depends as well as get_rosdeps - into a single result? That seems very use case specific.

The method get_licenses in this PR returns the dictionary of licenses and the packages. I found that useful that the output is not just a list of licenses but also the name of all packages in the dependency tree.

I found:

  • get_depends returns the list of upstream catkin packages.
  • get_rosdeps returns the list of upstream non-catkin packages.

In my usecase there's no reason to limit the scope of license introspection to catkin package (our customers want to know all the software installed on the delivered system). So including catking and non-catking packages actually important to me.

In general since the function only converts the results into a reverse mapping from license names to package names I am wondering how useful this is as part of the generic API of rospkg. Where do you plan to use the new API? Depending on the answer why can't the logic be implemented there instead?

I found this function convenient as a one-off to get all the list of licenses and all relevant packages (catkin and non-catkin packages), so that no other command is needed to get a complete list. Since in rospkg there's already get_depends, which returns a set of pkgs in the dependency tree, what get_licenses adds is essentially a license layer to the get_depends's output.

With the current implementation of this PR, capturing the licenses from the non-catkin packages is not included (e.g. what if the upstream implementation changes so that license of non-catkin package can be obtained?). I have a working code to capture licenses from system package (on Ubuntu) but due to the conversation with Dirk on previous PRs, I'm going with more granular PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants