repo_description: 🍊 :bar_chart: :bulb: Orange: Interactive data analysis
repo_topics: classification clustering data-mining data-science data-visualization decision-trees machine-learning numpy orange orange3 pandas plotting python random-forest regression scikit-learn scipy visual-programming visualization
code: #! /usr/bin/env python3
  
  import os
  import sys
  import subprocess
  from setuptools import setup, find_packages, Command
  from setuptools.command.install import install
  
  from distutils.command import install_data, sdist
  from distutils.command.build_ext import build_ext
  from distutils.command import config, build
  from distutils.core import Extension
  
  try:
      import numpy
      have_numpy = True
  except ImportError:
      have_numpy = False
  
  try:
      # need sphinx and recommonmark for build_htmlhelp command
      # pylint: disable=unused-import
      import sphinx
      import recommonmark
      have_sphinx = True
  except ImportError:
      have_sphinx = False
  
  try:
      from Cython.Build import cythonize
      have_cython = True
  except ImportError:
      have_cython = False
  
  NAME = 'Orange3'
  
  VERSION = '3.41.0'
  ISRELEASED = False
  # full version identifier including a git revision identifier for development
  # build/releases (this is filled/updated in `write_version_py`)
  FULLVERSION = VERSION
  
  DESCRIPTION = 'Orange, a component-based data mining framework.'
  README_FILE = os.path.join(os.path.dirname(__file__), 'README.pypi')
  LONG_DESCRIPTION = open(README_FILE).read()
  LONG_DESCRIPTION_CONTENT_TYPE = 'text/markdown'
  AUTHOR = 'Bioinformatics Laboratory, FRI UL'
  AUTHOR_EMAIL = '[REDACTED_EMAIL]'
  URL = 'https://orangedatamining.com/'
  PROJECT_URLS = {
      'Documentation': 'https://orangedatamining.com/docs',
      'Source Code': 'https://github.com/biolab/orange3',
      'Issue Tracker': 'https://github.com/biolab/orange3/issues',
      'Donate': 'https://github.com/sponsors/biolab'
  }
  LICENSE = 'GPLv3+'
  
  KEYWORDS = [
      'data mining',
      'machine learning',
      'artificial intelligence',
  ]
  
  CLASSIFIERS = [
      'Development Status :: 4 - Beta',
      'Environment :: X11 Applications :: Qt',
      'Environment :: Console',
      'Environment :: Plugins',
      'Programming Language :: Python',
      'License :: OSI Approved :: '
      'GNU General Public License v3 or later (GPLv3+)',
      'Operating System :: POSIX',
      'Operating System :: Microsoft :: Windows',
      'Topic :: Scientific/Engineering :: Artificial Intelligence',
      'Topic :: Scientific/Engineering :: Visualization',
      'Topic :: Software Development :: Libraries :: Python Modules',
      'Intended Audience :: Education',
      'Intended Audience :: Science/Research',
      'Intended Audience :: Developers',
  ]
  
  PYTHON_REQUIRES = ">=3.11"
  
  
  requirements = ['requirements-core.txt', 'requirements-gui.txt']
  
  
  INSTALL_REQUIRES = sorted(set(
      line.partition('#')[0].strip()
      for file in (os.path.join(os.path.dirname(__file__), file)
                   for file in requirements)
      for line in open(file)
  ) - {''})
  
  
  EXTRAS_REQUIRE = {}
  
  ENTRY_POINTS = {
      "orange.widgets": (
          "Orange Widgets = Orange.widgets",
      ),
      "orange.canvas.help": (
          "html-index = Orange.widgets:WIDGET_HELP_PATH",
      ),
      "orange.canvas.drophandler": (
          "File = Orange.widgets.data.owfile:OWFileDropHandler",
          "Load Model = Orange.widgets.model.owloadmodel:OWLoadModelDropHandler",
          "Distance File = Orange.widgets.unsupervised.owdistancefile:OWDistanceFileDropHandler",
          "Python Script = Orange.widgets.data.owpythonscript:OWPythonScriptDropHandler",
      ),
      "gui_scripts": (
          "orange-canvas = Orange.canvas.__main__:main",
      ),
  }
  
  
  DATA_FILES = []
  
  # Return the git revision as a string
  def git_version():
      """Return the git revision as a string.
  
      Copied from numpy setup.py
      """
      def _minimal_ext_cmd(cmd):
          # construct minimal environment
          env = {}
          for k in ['SYSTEMROOT', 'PATH']:
              v = os.environ.get(k)
              if v is not None:
                  env[k] = v
          # LANGUAGE is used on win32
          env['LANGUAGE'] = 'C'
          env['LANG'] = 'C'
          env['LC_ALL'] = 'C'
          out = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=env).communicate()[0]
          return out
  
      try:
          out = _minimal_ext_cmd(['git', 'rev-parse', 'HEAD'])
          GIT_REVISION = out.strip().decode('ascii')
      except OSError:
          GIT_REVISION = "Unknown"
      return GIT_REVISION
  
  
  def write_version_py(filename='Orange/version.py'):
      # Copied from numpy setup.py
      cnt = """
  # THIS FILE IS GENERATED FROM ORANGE SETUP.PY
  short_version = '%(version)s'
  version = '%(version)s'
  full_version = '%(full_version)s'
  git_revision = '%(git_revision)s'
  release = %(isrelease)s
  
  if not release:
      version = full_version
      short_version += ".dev"
  """
      global FULLVERSION
      FULLVERSION = VERSION
      if os.path.exists('.git'):
          GIT_REVISION = git_version()
      elif os.path.exists('Orange/version.py'):
          # must be a source distribution, use existing version file
          import importlib.util
          spec = importlib.util.spec_from_file_location(
              "Orange.version", filename
          )
          version = importlib.util.module_from_spec(spec)
          spec.loader.exec_module(version)
          GIT_REVISION = version.git_revision
      else:
          GIT_REVISION = "Unknown"
  
      if not ISRELEASED:
          FULLVERSION += '.dev0+' + GIT_REVISION[:7]
  
      a = open(filename, 'w')
      try:
          a.write(cnt % {'version': VERSION,
                         'full_version': FULLVERSION,
                         'git_revision': GIT_REVISION,
                         'isrelease': str(ISRELEASED)})
      finally:
          a.close()
  
  
  PACKAGES = find_packages(include=("Orange*",))
  
  # Extra non .py, .{so,pyd} files that are installed within the package dir
  # hierarchy
  PACKAGE_DATA = {
      "Orange": ["datasets/*.{}".format(ext)
                 for ext in ["tab", "csv", "basket", "info", "dst", "metadata"]],
      "Orange.canvas": ["icons/*.png", "icons/*.svg"],
      "Orange.canvas.workflows": ["*.ows"],
      "Orange.widgets": ["icons/*.png",
                         "icons/*.svg"],
      "Orange.widgets.report": ["icons/*.svg", "*.html"],
      "Orange.widgets.tests": ["datasets/*.tab",
                               "workflows/*.ows"],
      "Orange.widgets.data": ["icons/*.svg",
                              "icons/paintdata/*.png",
                              "icons/paintdata/*.svg"],
      "Orange.widgets.data.tests": ["origin1/*.tab",
                                    "origin2/*.tab",
                                    "*.txt", "*.tab", "*.foo", "*.xlsx"],
      "Orange.widgets.evaluate": ["icons/*.svg"],
      "Orange.widgets.model": ["icons/*.svg"],
      "Orange.widgets.visualize": ["icons/*.svg"],
      "Orange.widgets.unsupervised": ["icons/*.svg"],
      "Orange.widgets.utils": ["_webview/*.js"],
      "Orange.tests": ["xlsx_files/*.xlsx", "datasets/*.tab",
                       "xlsx_files/*.xls",
                       "datasets/*.basket", "datasets/*.csv",
                       "datasets/*.pkl", "datasets/*.pkl.gz"]
  }
  
  
  class LintCommand(Command):
      """A setup.py lint subcommand developers can run locally."""
      description = "run code linter(s)"
      user_options = []
      initialize_options = finalize_options = lambda self: None
  
      def run(self):
          """Lint current branch compared to a reasonable master branch"""
          sys.exit(subprocess.call(r'''
          set -eu
          upstream="$(git remote -v |
                      awk '/[@\/]github.com[:\/]biolab\/orange3[\. ]/{ print $1; exit }')"
          git fetch -q $upstream master
          best_ancestor=$(git merge-base HEAD refs/remotes/$upstream/master)
          .github/workflows/check_pylint_diff.sh $best_ancestor
          ''', shell=True, cwd=os.path.dirname(os.path.abspath(__file__))))
  
  class CoverageCommand(Command):
      """A setup.py coverage subcommand developers can run locally."""
      description = "run code coverage"
      user_options = []
      initialize_options = finalize_options = lambda self: None
  
      def run(self):
          """Check coverage on current workdir"""
          sys.exit(subprocess.call(r'''
          coverage run --source=Orange -m unittest -v Orange.tests
          echo; echo
          coverage combine
          coverage report
          coverage html &&
              { echo; echo "See also: file://$(pwd)/htmlcov/index.html"; echo; }
          ''', shell=True, cwd=os.path.dirname(os.path.abspath(__file__))))
  
  
  class build_ext_error(build_ext):
      def initialize_options(self):
          raise SystemExit(
              "Cannot compile extensions. numpy and cython are required to "
              "build Orange."
          )
  
  
  # ${prefix} relative install path for html help files
  DATAROOTDIR = "share/help/en/orange3/htmlhelp"
  
  
  def findall(startdir, followlinks=False, ):
      files = (
          os.path.join(base, file)
          for base, dirs, files in os.walk(startdir, followlinks=followlinks)
          for file in files
      )
      return filter(os.path.isfile, files)
  
  
  def find_htmlhelp_files(subdir):
      data_files = []
      thisdir = os.path.dirname(__file__)
      sourcedir = os.path.join(thisdir, subdir)
      files = filter(
          # filter out meta files
          lambda path: not path.endswith((".hhc", ".hhk", ".hhp", ".stp")),
          findall(sourcedir)
      )
      for file in files:
          relpath = os.path.relpath(file, start=subdir)
          relsubdir = os.path.dirname(relpath)
          # path.join("a", "") results in "a/"; distutils install_data does not
          # accept paths that end with "/" on windows.
          if relsubdir:
              targetdir = os.path.join(DATAROOTDIR, relsubdir)
          else:
              targetdir = DATAROOTDIR
          assert not targetdir.endswith("/")
          data_files.append((targetdir, [file]))
      return data_files
  
  
  def add_with_option(option, help="", default=None, ):
      """
      A class decorator that adds a boolean --with(out)-option cmd line switch
      to a distutils.cmd.Command class
  
      Parameters
      ----------
      option : str
          Name of the option without the 'with-' part i.e. passing foo will
          create a `--with-foo` and `--without-foo` options
      help : str
          Help for `cmd --help`. This should document the positive option (i.e.
          --with-foo)
      default : Optional[bool]
          The default state.
  
      Returns
      -------
      command : Command
  
      Examples
      --------
      >>> @add_with_option("foo", "Build with foo enabled", default=False)
      >>> class foobuild(build):
      >>>    def run(self):
      >>>        if self.with_foo:
      >>>            ...
  
      """
      def decorator(cmdclass):
          # type: (Type[Command]) -> Type[Command]
          cmdclass.user_options = getattr(cmdclass, "user_options", []) + [
              ("with-" + option, None, help),
              ("without-" + option, None, ""),
          ]
          cmdclass.boolean_options = getattr(cmdclass, "boolean_options", []) + [
              ("with-" + option,),
          ]
          cmdclass.negative_opt = dict(
              getattr(cmdclass, "negative_opt", {}), **{
                  "without-" + option: "with-" + option
              }
          )
          setattr(cmdclass, "with_" + option, default)
          return cmdclass
      return decorator
  
  
  _HELP = "Build and include html help files in the distribution"
  
  
  @add_with_option("htmlhelp", _HELP)
  class config(config.config):
      # just record the with-htmlhelp option for sdist and build's default
      pass
  
  
  @add_with_option("htmlhelp", _HELP)
  class sdist(sdist.sdist):
      # build_htmlhelp to fill in distribution.data_files which are then included
      # in the source dist.
      sub_commands = sdist.sdist.sub_commands + [
          ("build_htmlhelp", lambda self: self.with_htmlhelp)
      ]
  
      def finalize_options(self):
          super().finalize_options()
          self.set_undefined_options(
              "config", ("with_htmlhelp", "with_htmlhelp")
          )
  
  
  @add_with_option("htmlhelp", _HELP)
  class build(build.build):
      sub_commands = build.build.sub_commands + [
          ("build_htmlhelp", lambda self: self.with_htmlhelp)
      ]
  
      def finalize_options(self):
          super().finalize_options()
          self.set_undefined_options(
              "config", ("with_htmlhelp", 'with_htmlhelp')
          )
  
  
  # Does the sphinx source for widget help exist the sources are in the checkout
  # but not in the source distribution (sdist). The sdist already contains
  # build html files.
  HAVE_SPHINX_SOURCE = os.path.isdir("doc/visual-programming/source")
  # Doest the build htmlhelp documentation exist
  HAVE_BUILD_HTML = os.path.exists("doc/visual-programming/build/htmlhelp/index.html")
  
  if have_sphinx and HAVE_SPHINX_SOURCE:
      class build_htmlhelp(Command):
          user_options = []
  
          def finalize_options(self):
              pass
  
          def initialize_options(self):
              self.build_dir = "doc/visual-programming/build"
  
          def run(self):
              subprocess.check_call([
                  "sphinx-build", "-b", "htmlhelp", "-d", "build/doctrees",
                  "-D", f"version={VERSION}",
                  "source", "build"
                  ],
                  cwd="doc/visual-programming"
              )
              helpdir = os.path.join(self.build_dir, "htmlhelp")
              files = find_htmlhelp_files(helpdir)
              # add the build files to distribution
              self.distribution.data_files.extend(files)
  
  else:
      # without sphinx we need the docs to be already build. i.e. from a
      # source dist build --with-htmlhelp
      class build_htmlhelp(Command):
          user_options = [('build-dir=', None, 'Build directory')]
          build_dir = None
  
          def initialize_options(self):
              self.build_dir = "doc/visual-programming/build"
  
          def finalize_options(self):
              pass
  
          def run(self):
              helpdir = os.path.join(self.build_dir, "htmlhelp")
              if not (os.path.isdir(helpdir)
                      and os.path.isfile(os.path.join(helpdir, "index.html"))):
                  self.warn("Sphinx is needed to build help files. Skipping.")
                  return
              files = find_htmlhelp_files(os.path.join(helpdir))
              # add the build files to distribution
              self.distribution.data_files.extend(files)
  
  
  def ext_modules():
      includes = []
      libraries = []
      if have_numpy:
          includes.append(numpy.get_include())
  
      if os.name == 'posix':
          libraries.append("m")
  
      modules = [
          Extension(
              "Orange.classification._simple_tree",
              sources=[
                  "Orange/classification/_simple_tree.c",
              ],
              include_dirs=includes,
              libraries=libraries,
              export_symbols=[
                  "build_tree", "destroy_tree", "new_node",
                  "predict_classification", "predict_regression"
              ]
          ),
          Extension(
              "Orange.widgets.utils._grid_density",
              sources=["Orange/widgets/utils/_grid_density.cpp"],
              language="c++",
              include_dirs=includes,
              libraries=libraries,
              export_symbols=["compute_density"],
          ),
      ]
  
      if have_cython:
          modules += cythonize(Extension(
              "*",
              ["Orange/*/*.pyx"],
              include_dirs=includes,
              libraries=libraries,
          ))
  
      return modules
  
  
  class InstallMultilingualCommand(install):
      def run(self):
          super().run()
          self.compile_to_multilingual()
  
      def compile_to_multilingual(self):
          # Import locally so that editable install won't require trubar
          # pylint: disable=import-outside-toplevel
          from trubar import translate
  
          package_dir = os.path.dirname(os.path.abspath(__file__))
          translate(
              "msgs.jaml",
              source_dir=os.path.join(self.install_lib, "Orange"),
              config_file=os.path.join(package_dir, "i18n", "trubar-config.yaml"))
  
  
  def setup_package():
      write_version_py()
      cmdclass = {
          'install': InstallMultilingualCommand,
          'lint': LintCommand,
          'coverage': CoverageCommand,
          'config': config,
          'sdist': sdist,
          'build': build,
          'build_htmlhelp': build_htmlhelp,
          # Use install_data from distutils, not numpy.distutils.
          # numpy.distutils insist all data files are installed in site-packages
          'install_data': install_data.install_data
      }
      if not (have_numpy and have_cython):
          # substitute a build_ext command with one that raises an error when
          # building. In order to fully support `pip install` we need to
          # survive a `./setup egg_info` without numpy so pip can properly
          # query our install dependencies
          cmdclass["build_ext"] = build_ext_error
  
      extra_args = {}
      setup(
          name=NAME,
          version=FULLVERSION,
          description=DESCRIPTION,
          long_description=LONG_DESCRIPTION,
          long_description_content_type=LONG_DESCRIPTION_CONTENT_TYPE,
          author=AUTHOR,
          author_email=AUTHOR_EMAIL,
          url=URL,
          project_urls=PROJECT_URLS,
          license=LICENSE,
          keywords=KEYWORDS,
          classifiers=CLASSIFIERS,
          packages=PACKAGES,
          ext_modules=ext_modules(),
          package_data=PACKAGE_DATA,
          data_files=DATA_FILES,
          install_requires=INSTALL_REQUIRES,
          extras_require=EXTRAS_REQUIRE,
          python_requires=PYTHON_REQUIRES,
          entry_points=ENTRY_POINTS,
          zip_safe=False,
          test_suite='Orange.tests.suite',
          cmdclass=cmdclass,
          **extra_args
      )
  
  
  if __name__ == '__main__':
      setup_package()
  